Repository: zulip/zulip

Issue:
Make Welcome Bot more interactive
At present, Welcome Bot gives the user lots of information all at once. It may be unappealing to process so much content when one is just starting to explore a new app. Also, the bot doesn't offer motivation to engage with it, which would draw the user into sending their first Zulip messages.

To try to address this, we should make the Welcome Bot more interactive. In can provide a small amount of key information to start with, offering to teach the user more upon request.

An initial set of messages to be implemented is described below. In general, we should make it easy to create additional message/response interactions.

# New initial messages
## For a non-admin user 

Hello, and welcome to Zulip! This is a private message from me, Welcome Bot. If you are new to Zulip, check out our [Getting started guide](https://zulip.com/help/getting-started-with-zulip)!

----------

I can help you get set up! Just click anywhere on this message or press `r` to reply.
Here are a few messages I understand: “**apps**”, “**edit profile**”, “**dark mode**”, ”**light mode**”, “**streams**”, and “**topics**”.

----------

If you would like more help, send “**help**”, or type `?` to learn about Zulip’s keyboard shortcuts.

----------

Notes:

- We should have tests to make sure the list of advertised messages matches the messages the bot actually understands.
- It should be possible to manually determine the order of commands in the message (i.e. not just alphabetical).

# Additional responses to user's messages

In general, I think it’s helpful to be a bit flexible about the keywords, but I’m generally not too concerned about getting all the possible variants of what people type.

We should always ignore capitalization and formatting.

## Apps

(also: `app`, `applications`, `[any word] apps`)

- You can [download](https://zulip.com/apps) the [mobile and desktop apps](https://zulip.com/apps). Zulip also works great in a browser.


## Edit profile

(also: `profile`)

- Go to [Profile settings](http://[org URL]/#settings/profile) to [add a profile picture](https://zulip.com/help/change-your-profile-picture) and [edit your profile information](https://zulip.com/help/edit-your-profile).

**Messages for “dark mode”, ”light mode”, “streams”, and “topics” will be specified in more detail once we have the initial framework.**


## Any messages not on the list
- I’m sorry, I did not understand your message. Please try one of the following commands: "**apps**”, “**edit profile**”, “**dark mode**”, ”**light mode**”, “**streams**”, and “**topics**”.

# Other messages

We don’t want to spam with it all the time, but we may want to remind the user about the commands the bot know. Ideally, we should probably do this every time the last reminder scrolls off the screen. It would also be OK to do it every N messages (we should experiment to find a good N). This could be done as a follow-up after we have the initial version.

- **Message (same as in the intro)**: Here are a few messages I understand: “**apps**”, “**edit profile**”, “**dark mode**”, ”**light mode**”, “**streams**”, and “**topics**”.

[CZO discussion thread](https://chat.zulip.org/#narrow/stream/101-design/topic/welcome.20bot)


Discussion:
Hello @zulip/server-bots, @zulip/server-onboarding members, this issue was labeled with the "area: onboarding", "area: bots" labels, so you may want to check it out!

<!-- areaLabelAddition -->

@zulipbot claim


> * It should be possible to manually determine the order of commands in the message (i.e. not just alphabetical).

I am unable to understand what this means.
@raghupalash I was just pointing out that the commands suggested by the bot (“apps”, “edit profile”, “dark mode”, ”light mode”, “streams”, and “topics”) are not in alphabetical order.
Okay, thanks @alya! Also, If you got the time, the PR is ready for a review(or atleast I think it is ready).
Awesome, thanks @raghupalash ! Here are the remaining messages to add:

## Dark mode, light mode

(also: `night mode`, `day mode`, `theme`, `mode`)

- Go to [Display settings](http://[org URL]/#settings/display-settings) to [switch between light and dark mode](https://zulip.com/help/night-mode), [pick your favorite emoji theme](https://zulip.com/help/emoji-and-emoticons#change-your-emoji-set), [change your language](https://zulip.com/help/change-your-language), and make other tweaks to your Zulip experience.

## Streams

(also: `channels`)

In Zulip, streams [determine who gets a message](https://zulip.com/help/streams-and-topics). They are similar to channels in other apps.

[Browse and subscribe to streams](http://[org URL]/#streams/all).

## Topics
In Zulip, topics [tell you what a message is about](https://zulip.com/help/streams-and-topics). They are light-weight subjects, very similar to the subject line of an email. 

Check out [Recent topics](http://[org URL]/#recent_topics) to see what's happening!

## Keyboard shortcuts
(also: `shortcuts`)
Zulip's [keyboard shortcuts](http://[org URL]/#keyboard-shortcuts) let you navigate the app quickly and efficiently.

Press `?` any time to see a [cheat sheet](http://[org URL]/#keyboard-shortcuts).

## Message formatting
(also: `format`, `formatting`)
Zulip uses [Markdown](https://zulip.com/help/format-your-message-using-markdown), an intuitive format for bold, italics, bulleted lists, and more. Click [here](http://[org URL]/#keyboard-shortcuts) for a cheat sheet.

<Is there a URL we can use in the message above to make sure it opens to the keyboard shortcuts tab?>

Check out our [messaging tips](https://zulip.com/help/messaging-tips) to learn about [emoji reactions](https://zulip.com/help/emoji-reactions), [code blocks](https://zulip.com/help/code-blocks) and much more!

## Help

(also: `?`)

Here are a few messages I understand: < list >

Check out our [Getting started guide](https://zulip.com/help/getting-started-with-zulip), or browse the [help documentation](https://zulip.com/help) to learn more!


The list of messages the bot says it understands becomes:

“apps”, “edit profile”, “dark mode”, ”light mode”, “streams”, “topics”, "message formatting", and "keyboard shortcuts".
I haven't checked whether this is currently the case, but for admins, we should keep the "We also have a guide for [Setting up your organization]({help_url})." line.
Alright, I'm on it. Btw I didn't touch the code that was related to organization setup, it was left as it is. 
> <Is there a URL we can use in the message above to make sure it opens to the keyboard shortcuts tab?>

did you mean the message formatting tab? I figured that we need to go to `#message-formatting`.
> > <Is there a URL we can use in the message above to make sure it opens to the keyboard shortcuts tab?>
> 
> did you mean the message formatting tab? I figured that we need to go to `#message-formatting`.

Ah, yeah, that's right -- thanks!

Resolving patch:
diff --git a/tools/lib/capitalization.py b/tools/lib/capitalization.py
--- a/tools/lib/capitalization.py
+++ b/tools/lib/capitalization.py
@@ -72,6 +72,7 @@
     r".zuliprc",
     r"__\w+\.\w+__",
     # Things using "I"
+    r"I understand",
     r"I say",
     r"I want",
     r"I'm",
diff --git a/zerver/lib/onboarding.py b/zerver/lib/onboarding.py
--- a/zerver/lib/onboarding.py
+++ b/zerver/lib/onboarding.py
@@ -68,38 +68,40 @@ def send_initial_pms(user: UserProfile) -> None:
             " " + _("We also have a guide for [Setting up your organization]({help_url}).")
         ).format(help_url=help_url)
 
-    welcome_msg = _("Hello, and welcome to Zulip!")
+    welcome_msg = _("Hello, and welcome to Zulip!") + "👋"
+    demo_org_warning = ""
     if user.realm.demo_organization_scheduled_deletion_date is not None:
-        welcome_msg += " " + _(
-            "Note that this is a [demo organization]({demo_org_help_url}) and will be automatically deleted in 30 days."
+        demo_org_warning = (
+            _(
+                "Note that this is a [demo organization]({demo_org_help_url}) and will be "
+                "**automatically deleted** in 30 days."
+            )
+            + "\n\n"
         )
 
     content = "".join(
         [
             welcome_msg + " ",
             _("This is a private message from me, Welcome Bot.") + "\n\n",
-            "* "
-            + _(
+            _(
                 "If you are new to Zulip, check out our [Getting started guide]({getting_started_url})!"
+            ),
+            "{organization_setup_text}" + "\n\n",
+            "{demo_org_warning}",
+            _(
+                "I can also help you get set up! Just click anywhere on this message or press `r` to reply."
             )
-            + "{organization_setup_text}\n",
-            "* " + _("[Add a profile picture]({profile_url}).") + "\n",
-            "* " + _("[Browse and subscribe to streams]({streams_url}).") + "\n",
-            "* " + _("Download our [mobile and desktop apps]({apps_url}).") + " ",
-            _("Zulip also works great in a browser.") + "\n",
-            "* " + _("You can type `?` to learn more about Zulip shortcuts.") + "\n\n",
-            _("Practice sending a few messages by replying to this conversation.") + " ",
-            _("Click anywhere on this message or press `r` to reply."),
+            + "\n\n",
+            _("Here are a few messages I understand:") + " ",
+            bot_commands(is_initial_pm=True),
         ]
     )
 
     content = content.format(
-        getting_started_url="/help/getting-started-with-zulip",
-        apps_url="/apps",
-        profile_url="#settings/profile",
-        streams_url="#streams/all",
         organization_setup_text=organization_setup_text,
+        demo_org_warning=demo_org_warning,
         demo_org_help_url="/help/demo-organizations",
+        getting_started_url="/help/getting-started-with-zulip",
     )
 
     internal_send_private_message(
@@ -107,22 +109,124 @@ def send_initial_pms(user: UserProfile) -> None:
     )
 
 
+def bot_commands(is_initial_pm: bool = False) -> str:
+    commands = [
+        "apps",
+        "edit profile",
+        "theme",
+        "streams",
+        "topics",
+        "message formatting",
+        "keyboard shortcuts",
+    ]
+    if is_initial_pm:
+        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 [Display settings](#settings/display-settings) "
+            "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 topics](#recent_topics) to see what's happening! "
+                    'You can return to this conversation by clicking "Private 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() + "\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 private 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_recipient_id = send_request.message.sender.recipient_id
-    assert human_recipient_id is not None
-    if Message.objects.filter(sender=welcome_bot, recipient_id=human_recipient_id).count() < 2:
-        content = (
-            _("Congratulations on your first reply!") + " "
-            ":tada:"
-            "\n"
-            "\n"
-            + _(
-                "Feel free to continue using this space to practice your new messaging "
-                "skills. Or, try clicking on some of the stream names to your left!"
-            )
-        )
-        internal_send_private_message(welcome_bot, send_request.message.sender, content)
+    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)
 
 
 @transaction.atomic


Test patch:
diff --git a/zerver/tests/test_tutorial.py b/zerver/tests/test_tutorial.py
--- a/zerver/tests/test_tutorial.py
+++ b/zerver/tests/test_tutorial.py
@@ -32,22 +32,136 @@ def test_tutorial_status(self) -> None:
             user = self.example_user("hamlet")
             self.assertEqual(user.tutorial_status, expected_db_status)
 
-    def test_single_response_to_pm(self) -> None:
+    def test_response_to_pm_for_app(self) -> None:
         user = self.example_user("hamlet")
         bot = get_system_bot(settings.WELCOME_BOT, user.realm_id)
-        content = "whatever"
+        messages = ["app", "Apps"]
+        self.login_user(user)
+        for content in messages:
+            self.send_personal_message(user, bot, content)
+            expected_response = (
+                "You can [download](/apps) the [mobile and desktop apps](/apps). "
+                "Zulip also works great in a browser."
+            )
+            self.assertEqual(most_recent_message(user).content, expected_response)
+
+    def test_response_to_pm_for_edit(self) -> None:
+        user = self.example_user("hamlet")
+        bot = get_system_bot(settings.WELCOME_BOT, user.realm_id)
+        messages = ["profile", "Profile"]
+        self.login_user(user)
+        for content in messages:
+            self.send_personal_message(user, bot, content)
+            expected_response = (
+                "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)."
+            )
+            self.assertEqual(most_recent_message(user).content, expected_response)
+
+    def test_response_to_pm_for_theme(self) -> None:
+        user = self.example_user("hamlet")
+        bot = get_system_bot(settings.WELCOME_BOT, user.realm_id)
+        messages = ["theme", "Theme"]
+        self.login_user(user)
+        for content in messages:
+            self.send_personal_message(user, bot, content)
+            expected_response = (
+                "Go to [Display settings](#settings/display-settings) "
+                "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."
+            )
+            self.assertEqual(most_recent_message(user).content, expected_response)
+
+    def test_response_to_pm_for_stream(self) -> None:
+        user = self.example_user("hamlet")
+        bot = get_system_bot(settings.WELCOME_BOT, user.realm_id)
+        messages = ["Streams", "streams", "channels"]
+        self.login_user(user)
+        for content in messages:
+            self.send_personal_message(user, bot, content)
+            expected_response = (
+                "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)."
+            )
+            self.assertEqual(most_recent_message(user).content, expected_response)
+
+    def test_response_to_pm_for_topic(self) -> None:
+        user = self.example_user("hamlet")
+        bot = get_system_bot(settings.WELCOME_BOT, user.realm_id)
+        messages = ["Topics", "topics"]
+        self.login_user(user)
+        for content in messages:
+            self.send_personal_message(user, bot, content)
+            expected_response = (
+                "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 topics](#recent_topics) to see what's happening! "
+                'You can return to this conversation by clicking "Private messages" in the upper left.'
+            )
+            self.assertEqual(most_recent_message(user).content, expected_response)
+
+    def test_response_to_pm_for_shortcuts(self) -> None:
+        user = self.example_user("hamlet")
+        bot = get_system_bot(settings.WELCOME_BOT, user.realm_id)
+        messages = ["Keyboard shortcuts", "shortcuts", "Shortcuts"]
+        self.login_user(user)
+        for content in messages:
+            self.send_personal_message(user, bot, content)
+            expected_response = (
+                "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)."
+            )
+            self.assertEqual(most_recent_message(user).content, expected_response)
+
+    def test_response_to_pm_for_formatting(self) -> None:
+        user = self.example_user("hamlet")
+        bot = get_system_bot(settings.WELCOME_BOT, user.realm_id)
+        messages = ["message formatting", "Formatting"]
+        self.login_user(user)
+        for content in messages:
+            self.send_personal_message(user, bot, content)
+            expected_response = (
+                "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!"
+            )
+            self.assertEqual(most_recent_message(user).content, expected_response)
+
+    def test_response_to_pm_for_help(self) -> None:
+        user = self.example_user("hamlet")
+        bot = get_system_bot(settings.WELCOME_BOT, user.realm_id)
+        messages = ["help", "Help", "?"]
+        self.login_user(user)
+        for content in messages:
+            self.send_personal_message(user, bot, content)
+            expected_response = (
+                "Here are a few messages I understand: "
+                "`apps`, `edit profile`, `theme`, "
+                "`streams`, `topics`, `message formatting`, `keyboard shortcuts`.\n\n"
+                "Check out our [Getting started guide](/help/getting-started-with-zulip), "
+                "or browse the [Help center](/help/) to learn more!"
+            )
+            self.assertEqual(most_recent_message(user).content, expected_response)
+
+    def test_response_to_pm_for_undefined(self) -> None:
+        user = self.example_user("hamlet")
+        bot = get_system_bot(settings.WELCOME_BOT, user.realm_id)
+        messages = ["Hello", "HAHAHA", "OKOK", "LalulaLapas"]
         self.login_user(user)
-        self.send_personal_message(user, bot, content)
-        user_messages = message_stream_count(user)
-        expected_response = (
-            "Congratulations on your first reply! :tada:\n\n"
-            "Feel free to continue using this space to practice your new messaging "
-            "skills. Or, try clicking on some of the stream names to your left!"
-        )
-        self.assertEqual(most_recent_message(user).content, expected_response)
-        # Welcome bot shouldn't respond to further PMs.
-        self.send_personal_message(user, bot, content)
-        self.assertEqual(message_stream_count(user), user_messages + 1)
+        for content in messages:
+            self.send_personal_message(user, bot, content)
+            expected_response = (
+                "I’m sorry, I did not understand your message. Please try one of the following commands: "
+                "`apps`, `edit profile`, `theme`, `streams`, "
+                "`topics`, `message formatting`, `keyboard shortcuts`."
+            )
+            self.assertEqual(most_recent_message(user).content, expected_response)
 
     def test_no_response_to_group_pm(self) -> None:
         user1 = self.example_user("hamlet")