diff --git a/documentation/docs/clients/whatsapp.md b/documentation/docs/clients/whatsapp.md
new file mode 100644
index 00000000..fc0d38e1
--- /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 7f969f3a..4b0c4140 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 1b5e4d1b..8089ee40 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 @@ class ClientApplicationAdapters:
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 @@ class ConversationAdapters:
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 521e81de..9b07efde 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 00000000..88b4b140
--- /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 93f4f2ac..688ca9af 100644
--- a/src/khoj/database/models/__init__.py
+++ b/src/khoj/database/models/__init__.py
@@ -26,6 +26,7 @@ class ClientApplication(BaseModel):
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 00000000..28b171c0
--- /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 b3a5a3a3..285936b6 100644
--- a/src/khoj/interface/web/base_config.html
+++ b/src/khoj/interface/web/base_config.html
@@ -7,6 +7,11 @@