From 679db51453eeed25cb81bd443e4e9fc0889e3266 Mon Sep 17 00:00:00 2001
From: sabaimran <65192171+sabaimran@users.noreply.github.com>
Date: Mon, 22 Jan 2024 18:14:58 -0800
Subject: [PATCH] Add support for phone number authentication with Khoj (part
2) (#621)
* Allow users to configure phone numbers with the Khoj server
* Integration of API endpoint for updating phone number
* Add phone number association and OTP via Twilio for users connecting to WhatsApp
- When verified, store the result as such in the KhojUser object
* Add a Whatsapp.svg for configuring phone number
* Change setup hint depending on whether the user has a number already connected or not
* Add an integrity check for the intl tel js dependency
* Customize the UI based on whether the user has verified their phone number
- Update API routes to make nomenclature for phone addition and verification more straightforward (just /config/phone, etc).
- If user has not verified, prompt them for another verification code (if verification is enabled) in the configuration page
* Use the verified filter only if the user is linked to an account with an email
* Add some basic documentation for using the WhatsApp client with Khoj
* Point help text to the docs, rather than landing page info
* Update messages on various callbacks and add link to docs page to learn more about the integration
---
documentation/docs/clients/whatsapp.md | 28 +++
pyproject.toml | 1 +
src/khoj/database/adapters/__init__.py | 57 ++++-
src/khoj/database/admin.py | 1 +
.../0028_khojuser_verified_phone_number.py | 17 ++
src/khoj/database/models/__init__.py | 1 +
.../interface/web/assets/icons/whatsapp.svg | 17 ++
src/khoj/interface/web/base_config.html | 28 +++
src/khoj/interface/web/config.html | 207 ++++++++++++++++++
src/khoj/processor/conversation/utils.py | 5 +-
src/khoj/routers/api.py | 13 +-
src/khoj/routers/api_config.py | 73 +++++-
src/khoj/routers/helpers.py | 2 +
src/khoj/routers/twilio.py | 36 +++
src/khoj/routers/web_client.py | 5 +-
15 files changed, 478 insertions(+), 13 deletions(-)
create mode 100644 documentation/docs/clients/whatsapp.md
create mode 100644 src/khoj/database/migrations/0028_khojuser_verified_phone_number.py
create mode 100644 src/khoj/interface/web/assets/icons/whatsapp.svg
create mode 100644 src/khoj/routers/twilio.py
diff --git a/documentation/docs/clients/whatsapp.md b/documentation/docs/clients/whatsapp.md
new file mode 100644
index 000000000..fc0d38e1a
--- /dev/null
+++ b/documentation/docs/clients/whatsapp.md
@@ -0,0 +1,28 @@
+---
+sidebar_position: 5
+---
+
+# WhatsApp
+
+> Query your Second Brain from WhatsApp
+
+Text [+1 (848) 800 4242](https://wa.me/18488004242) or scan [this QR code](https://khoj.dev/whatsapp) on your phone to chat with Khoj on WhatsApp.
+
+Without any desktop clients, you can start chatting with Khoj on WhatsApp. Bear in mind you do need one of the desktop clients in order to share and sync your data with Khoj. The WhatsApp AI bot will work right away for answering generic queries and using Khoj in default mode.
+
+In order to use Khoj on WhatsApp with your own data, you need to setup a Khoj Cloud account and connect your WhatsApp account to it. This is a one time setup and you can do it from the [Khoj Cloud config page](https://app.khoj.dev/config).
+
+If you hit usage limits for the WhatsApp bot, upgrade to [a paid plan](https://khoj.dev/pricing) on Khoj Cloud.
+
+## Features
+
+- **Slash Commands**: Use slash commands to quickly access Khoj features
+ - `/online`: Get responses from Khoj powered by online search.
+ - `/dream`: Generate an image in response to your prompt.
+ - `/notes`: Explicitly force Khoj to retrieve context from your notes. Note: You'll need to connect your WhatsApp account to a Khoj Cloud account for this to work.
+
+We have more commands under development, including `/share` to uploading documents directly to your Khoj account from WhatsApp, and `/speak` in order to get a speech response from Khoj. Feel free to [raise an issue](https://github.com/khoj-ai/flint/issues) if you have any suggestions for new commands.
+
+## Nerdy Details
+
+You can find all of the code for the WhatsApp bot in the the [flint repository](https://github.com/khoj-ai/flint). As all of our code, it is open source and you can contribute to it.
diff --git a/pyproject.toml b/pyproject.toml
index 7f969f3af..4b0c41403 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -78,6 +78,7 @@ dependencies = [
"openai-whisper >= 20231117",
"django-phonenumber-field == 7.3.0",
"phonenumbers == 8.13.27",
+ "twilio == 8.11"
]
dynamic = ["version"]
diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py
index 1b5e4d1be..8089ee400 100644
--- a/src/khoj/database/adapters/__init__.py
+++ b/src/khoj/database/adapters/__init__.py
@@ -95,6 +95,36 @@ async def aget_or_create_user_by_phone_number(phone_number: str) -> KhojUser:
return user
+async def aset_user_phone_number(user: KhojUser, phone_number: str) -> KhojUser:
+ if is_none_or_empty(phone_number):
+ return None
+ phone_number = phone_number.strip()
+ if not phone_number.startswith("+"):
+ phone_number = f"+{phone_number}"
+ existing_user_with_phone_number = await aget_user_by_phone_number(phone_number)
+ if existing_user_with_phone_number and existing_user_with_phone_number.id != user.id:
+ if is_none_or_empty(existing_user_with_phone_number.email):
+ # Transfer conversation history to the new user. If they don't have an associated email, they are effectively a new user
+ async for conversation in Conversation.objects.filter(user=existing_user_with_phone_number).aiterator():
+ conversation.user = user
+ await conversation.asave()
+
+ await existing_user_with_phone_number.adelete()
+ else:
+ raise HTTPException(status_code=400, detail="Phone number already exists")
+
+ user.phone_number = phone_number
+ await user.asave()
+ return user
+
+
+async def aremove_phone_number(user: KhojUser) -> KhojUser:
+ user.phone_number = None
+ user.verified_phone_number = False
+ await user.asave()
+ return user
+
+
async def acreate_user_by_phone_number(phone_number: str) -> KhojUser:
if is_none_or_empty(phone_number):
return None
@@ -213,7 +243,20 @@ async def get_user_by_token(token: dict) -> KhojUser:
async def aget_user_by_phone_number(phone_number: str) -> KhojUser:
if is_none_or_empty(phone_number):
return None
- return await KhojUser.objects.filter(phone_number=phone_number).prefetch_related("subscription").afirst()
+ matched_user = await KhojUser.objects.filter(phone_number=phone_number).prefetch_related("subscription").afirst()
+
+ if not matched_user:
+ return None
+
+ # If the user with this phone number does not have an email account with Khoj, return the user
+ if is_none_or_empty(matched_user.email):
+ return matched_user
+
+ # If the user has an email account with Khoj and a verified number, return the user
+ if matched_user.verified_phone_number:
+ return matched_user
+
+ return None
async def retrieve_user(session_id: str) -> KhojUser:
@@ -307,11 +350,11 @@ async def aget_client_application_by_id(client_id: str, client_secret: str):
class ConversationAdapters:
@staticmethod
- def get_conversation_by_user(user: KhojUser):
- conversation = Conversation.objects.filter(user=user)
+ def get_conversation_by_user(user: KhojUser, client_application: ClientApplication = None):
+ conversation = Conversation.objects.filter(user=user, client=client_application)
if conversation.exists():
return conversation.first()
- return Conversation.objects.create(user=user)
+ return Conversation.objects.create(user=user, client=client_application)
@staticmethod
async def aget_conversation_by_user(user: KhojUser, client_application: ClientApplication = None):
@@ -383,12 +426,12 @@ async def aget_default_conversation_config():
return await ChatModelOptions.objects.filter().afirst()
@staticmethod
- def save_conversation(user: KhojUser, conversation_log: dict):
- conversation = Conversation.objects.filter(user=user)
+ def save_conversation(user: KhojUser, conversation_log: dict, client_application: ClientApplication = None):
+ conversation = Conversation.objects.filter(user=user, client=client_application)
if conversation.exists():
conversation.update(conversation_log=conversation_log)
else:
- Conversation.objects.create(user=user, conversation_log=conversation_log)
+ Conversation.objects.create(user=user, conversation_log=conversation_log, client=client_application)
@staticmethod
def get_conversation_processor_options():
diff --git a/src/khoj/database/admin.py b/src/khoj/database/admin.py
index 521e81dea..9b07efdea 100644
--- a/src/khoj/database/admin.py
+++ b/src/khoj/database/admin.py
@@ -58,6 +58,7 @@ class ConversationAdmin(admin.ModelAdmin):
"user",
"created_at",
"updated_at",
+ "client",
)
search_fields = ("conversation_id",)
ordering = ("-created_at",)
diff --git a/src/khoj/database/migrations/0028_khojuser_verified_phone_number.py b/src/khoj/database/migrations/0028_khojuser_verified_phone_number.py
new file mode 100644
index 000000000..88b4b1402
--- /dev/null
+++ b/src/khoj/database/migrations/0028_khojuser_verified_phone_number.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.7 on 2024-01-19 13:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("database", "0027_merge_20240118_1324"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="khojuser",
+ name="verified_phone_number",
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/src/khoj/database/models/__init__.py b/src/khoj/database/models/__init__.py
index 93f4f2ac5..688ca9af9 100644
--- a/src/khoj/database/models/__init__.py
+++ b/src/khoj/database/models/__init__.py
@@ -26,6 +26,7 @@ def __str__(self):
class KhojUser(AbstractUser):
uuid = models.UUIDField(models.UUIDField(default=uuid.uuid4, editable=False))
phone_number = PhoneNumberField(null=True, default=None, blank=True)
+ verified_phone_number = models.BooleanField(default=False)
def save(self, *args, **kwargs):
if not self.uuid:
diff --git a/src/khoj/interface/web/assets/icons/whatsapp.svg b/src/khoj/interface/web/assets/icons/whatsapp.svg
new file mode 100644
index 000000000..28b171c00
--- /dev/null
+++ b/src/khoj/interface/web/assets/icons/whatsapp.svg
@@ -0,0 +1,17 @@
+
+
+
diff --git a/src/khoj/interface/web/base_config.html b/src/khoj/interface/web/base_config.html
index b3a5a3a3c..285936b6d 100644
--- a/src/khoj/interface/web/base_config.html
+++ b/src/khoj/interface/web/base_config.html
@@ -7,6 +7,11 @@