Skip to content

Commit

Permalink
Add support for phone number authentication with Khoj (part 2) (#621)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sabaimran authored Jan 23, 2024
1 parent 58bf917 commit 679db51
Show file tree
Hide file tree
Showing 15 changed files with 478 additions and 13 deletions.
28 changes: 28 additions & 0 deletions documentation/docs/clients/whatsapp.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ dependencies = [
"openai-whisper >= 20231117",
"django-phonenumber-field == 7.3.0",
"phonenumbers == 8.13.27",
"twilio == 8.11"
]
dynamic = ["version"]

Expand Down
57 changes: 50 additions & 7 deletions src/khoj/database/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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():
Expand Down
1 change: 1 addition & 0 deletions src/khoj/database/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class ConversationAdmin(admin.ModelAdmin):
"user",
"created_at",
"updated_at",
"client",
)
search_fields = ("conversation_id",)
ordering = ("-created_at",)
Expand Down
Original file line number Diff line number Diff line change
@@ -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),
),
]
1 change: 1 addition & 0 deletions src/khoj/database/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
17 changes: 17 additions & 0 deletions src/khoj/interface/web/assets/icons/whatsapp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions src/khoj/interface/web/base_config.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
<title>Khoj - Settings</title>
<link rel="stylesheet" href="/static/assets/pico.min.css">
<link rel="stylesheet" href="/static/assets/khoj.css">
<script
integrity="sha384-05IkdNHoAlkhrFVUCCN805WC/h4mcI98GUBssmShF2VJAXKyZTrO/TmJ+4eBo0Cy"
crossorigin="anonymous"
src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.13/js/intlTelInput.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.13/css/intlTelInput.css">
</head>
<script type="text/javascript" src="/static/assets/utils.js"></script>
<body class="khoj-configure">
Expand Down Expand Up @@ -330,6 +335,29 @@
text-decoration: none;
}

div#phone-number-input-element {
display: flex;
align-items: center;
}

p#phone-number-plus {
padding: 8px;
}

div#clients {
grid-gap: 12px;
}

input#country-code-phone-number-input {
max-width: 100px;
margin-right: 8px;
}

input#country-code-phone-number-input {
max-width: 100px;
margin-right: 8px;
}

@media screen and (max-width: 700px) {
.section-cards {
grid-template-columns: 1fr;
Expand Down
Loading

0 comments on commit 679db51

Please sign in to comment.