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
This commit is contained in:
sabaimran 2024-01-22 18:14:58 -08:00 committed by GitHub
parent 58bf917775
commit 679db51453
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 478 additions and 13 deletions

View file

@ -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.

View file

@ -78,6 +78,7 @@ dependencies = [
"openai-whisper >= 20231117", "openai-whisper >= 20231117",
"django-phonenumber-field == 7.3.0", "django-phonenumber-field == 7.3.0",
"phonenumbers == 8.13.27", "phonenumbers == 8.13.27",
"twilio == 8.11"
] ]
dynamic = ["version"] dynamic = ["version"]

View file

@ -95,6 +95,36 @@ async def aget_or_create_user_by_phone_number(phone_number: str) -> KhojUser:
return user 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: async def acreate_user_by_phone_number(phone_number: str) -> KhojUser:
if is_none_or_empty(phone_number): if is_none_or_empty(phone_number):
return None 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: async def aget_user_by_phone_number(phone_number: str) -> KhojUser:
if is_none_or_empty(phone_number): if is_none_or_empty(phone_number):
return None 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: async def retrieve_user(session_id: str) -> KhojUser:
@ -307,11 +350,11 @@ class ClientApplicationAdapters:
class ConversationAdapters: class ConversationAdapters:
@staticmethod @staticmethod
def get_conversation_by_user(user: KhojUser): def get_conversation_by_user(user: KhojUser, client_application: ClientApplication = None):
conversation = Conversation.objects.filter(user=user) conversation = Conversation.objects.filter(user=user, client=client_application)
if conversation.exists(): if conversation.exists():
return conversation.first() return conversation.first()
return Conversation.objects.create(user=user) return Conversation.objects.create(user=user, client=client_application)
@staticmethod @staticmethod
async def aget_conversation_by_user(user: KhojUser, client_application: ClientApplication = None): async def aget_conversation_by_user(user: KhojUser, client_application: ClientApplication = None):
@ -383,12 +426,12 @@ class ConversationAdapters:
return await ChatModelOptions.objects.filter().afirst() return await ChatModelOptions.objects.filter().afirst()
@staticmethod @staticmethod
def save_conversation(user: KhojUser, conversation_log: dict): def save_conversation(user: KhojUser, conversation_log: dict, client_application: ClientApplication = None):
conversation = Conversation.objects.filter(user=user) conversation = Conversation.objects.filter(user=user, client=client_application)
if conversation.exists(): if conversation.exists():
conversation.update(conversation_log=conversation_log) conversation.update(conversation_log=conversation_log)
else: else:
Conversation.objects.create(user=user, conversation_log=conversation_log) Conversation.objects.create(user=user, conversation_log=conversation_log, client=client_application)
@staticmethod @staticmethod
def get_conversation_processor_options(): def get_conversation_processor_options():

View file

@ -58,6 +58,7 @@ class ConversationAdmin(admin.ModelAdmin):
"user", "user",
"created_at", "created_at",
"updated_at", "updated_at",
"client",
) )
search_fields = ("conversation_id",) search_fields = ("conversation_id",)
ordering = ("-created_at",) ordering = ("-created_at",)

View file

@ -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),
),
]

View file

@ -26,6 +26,7 @@ class ClientApplication(BaseModel):
class KhojUser(AbstractUser): class KhojUser(AbstractUser):
uuid = models.UUIDField(models.UUIDField(default=uuid.uuid4, editable=False)) uuid = models.UUIDField(models.UUIDField(default=uuid.uuid4, editable=False))
phone_number = PhoneNumberField(null=True, default=None, blank=True) phone_number = PhoneNumberField(null=True, default=None, blank=True)
verified_phone_number = models.BooleanField(default=False)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.uuid: if not self.uuid:

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Whatsapp-color</title>
<desc>Created with Sketch.</desc>
<defs>
</defs>
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Color-" transform="translate(-700.000000, -360.000000)" fill="#67C15E">
<path d="M723.993033,360 C710.762252,360 700,370.765287 700,383.999801 C700,389.248451 701.692661,394.116025 704.570026,398.066947 L701.579605,406.983798 L710.804449,404.035539 C714.598605,406.546975 719.126434,408 724.006967,408 C737.237748,408 748,397.234315 748,384.000199 C748,370.765685 737.237748,360.000398 724.006967,360.000398 L723.993033,360.000398 L723.993033,360 Z M717.29285,372.190836 C716.827488,371.07628 716.474784,371.034071 715.769774,371.005401 C715.529728,370.991464 715.262214,370.977527 714.96564,370.977527 C714.04845,370.977527 713.089462,371.245514 712.511043,371.838033 C711.806033,372.557577 710.056843,374.23638 710.056843,377.679202 C710.056843,381.122023 712.567571,384.451756 712.905944,384.917648 C713.258648,385.382743 717.800808,392.55031 724.853297,395.471492 C730.368379,397.757149 732.00491,397.545307 733.260074,397.27732 C735.093658,396.882308 737.393002,395.527239 737.971421,393.891043 C738.54984,392.25405 738.54984,390.857171 738.380255,390.560912 C738.211068,390.264652 737.745308,390.095816 737.040298,389.742615 C736.335288,389.389811 732.90737,387.696673 732.25849,387.470894 C731.623543,387.231179 731.017259,387.315995 730.537963,387.99333 C729.860819,388.938653 729.198006,389.89831 728.661785,390.476494 C728.238619,390.928051 727.547144,390.984595 726.969123,390.744481 C726.193254,390.420348 724.021298,389.657798 721.340985,387.273388 C719.267356,385.42535 717.856938,383.125756 717.448104,382.434484 C717.038871,381.729275 717.405907,381.319529 717.729948,380.938852 C718.082653,380.501232 718.421026,380.191036 718.77373,379.781688 C719.126434,379.372738 719.323884,379.160897 719.549599,378.681068 C719.789645,378.215575 719.62006,377.735746 719.450874,377.382942 C719.281687,377.030139 717.871269,373.587317 717.29285,372.190836 Z" id="Whatsapp">
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -7,6 +7,11 @@
<title>Khoj - Settings</title> <title>Khoj - Settings</title>
<link rel="stylesheet" href="/static/assets/pico.min.css"> <link rel="stylesheet" href="/static/assets/pico.min.css">
<link rel="stylesheet" href="/static/assets/khoj.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> </head>
<script type="text/javascript" src="/static/assets/utils.js"></script> <script type="text/javascript" src="/static/assets/utils.js"></script>
<body class="khoj-configure"> <body class="khoj-configure">
@ -330,6 +335,29 @@
text-decoration: none; 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) { @media screen and (max-width: 700px) {
.section-cards { .section-cards {
grid-template-columns: 1fr; grid-template-columns: 1fr;

View file

@ -194,6 +194,37 @@
</button> </button>
</div> </div>
</div> </div>
<div id="phone-number-input-id" class="api-settings">
<div class="card-title-row">
<img class="card-icon" src="/static/assets/icons/whatsapp.svg" alt="WhatsApp icon">
<h3 class="card-title">WhatsApp</h3>
</div>
<div class="card-description-row">
<p id="api-settings-card-description-verified" class="card-description" style="{{ 'display: none;' if not phone_number else '' }}">Your number is connected. You can now chat with Khoj on WhatsApp at <a href="https://wa.me/18488004242">+1-848-800-4242</a>. Learn more about the integration <a href="https://docs.khoj.dev/clients/whatsapp">here</a>.</p>
<p id="api-settings-card-description-unverified" class="card-description" style="{{ 'display: none;' if phone_number else '' }}">Connect your number to chat with Khoj on WhatsApp. Learn more about the integration <a href="https://docs.khoj.dev/clients/whatsapp">here</a>.</p>
</div>
<div id="phone-number-input-element" class="card-action-row">
{% if phone_number %}
<input type="text" id="mobile_code" class="form-control" placeholder="Phone Number" name="name" value="{{ phone_number }}">
{% else %}
<input type="text" id="mobile_code" class="form-control" placeholder="Phone Number" name="name">
{% endif %}
</div>
<div id="phone-number-update-callback" class="card-action-row" style="display: none;">
</div>
<div class="card-action-row">
<button id="whatsapp-verify" class="card-button happy" style="display: none;">
Get OTP on WhatsApp and Verify
</button>
<button id="whatsapp-remove" class="card-button" style="display: none;">
Remove
</button>
<input type="number" maxlength="6" id="whatsapp_otp" class="whatsapp_otp" placeholder="OTP" name="otp_code" style="display: none;">
<button id="whatsapp-verify-otp" class="card-button happy" style="display: none;">
Verify OTP
</button>
</div>
</div>
</div> </div>
{% if billing_enabled %} {% if billing_enabled %}
<div id="billing" class="section"> <div id="billing" class="section">
@ -555,5 +586,181 @@
} }
}) })
} }
var phoneInputField = document.querySelector("#mobile_code");
const iti = window.intlTelInput(phoneInputField, {
initialCountry: "auto",
geoIpLookup: callback => {
fetch("https://ipapi.co/json")
.then(res => res.json())
.then(data => callback(data.country_code))
.catch(() => callback("us"))
},
separateDialCode: true,
utilsScript: "https://cdn.jsdelivr.net/npm/intl-tel-input@18.2.1/build/js/utils.js",
});
const errorMap = ["Invalid number", "Invalid country code", "Too short", "Too long", "Invalid number"];
const phoneNumberUpdateCallback = document.getElementById("phone-number-update-callback");
const phonenumberVerifyButton = document.getElementById("whatsapp-verify");
const phonenumberRemoveButton = document.getElementById("whatsapp-remove");
const phonenumberVerifyOTPButton = document.getElementById("whatsapp-verify-otp");
const phonenumberOTPInput = document.getElementById("whatsapp_otp");
const phonenumberVerifiedText = document.getElementById("api-settings-card-description-verified");
const phonenumberUnverifiedText = document.getElementById("api-settings-card-description-unverified");
let preExistingPhoneNumber = "{{ phone_number }}";
let isPhoneNumberVerified = "{{ is_phone_number_verified }}";
let isTwilioEnabled = "{{ is_twilio_enabled }}";
if (preExistingPhoneNumber != "None" && (isPhoneNumberVerified == "True" || isPhoneNumberVerified == "true")) {
phonenumberVerifyButton.style.display = "none";
phonenumberRemoveButton.style.display = "";
} else if (preExistingPhoneNumber != "None" && (isPhoneNumberVerified == "False" || isPhoneNumberVerified == "false")) {
if (isTwilioEnabled == "True" || isTwilioEnabled == "true") {
phonenumberVerifyButton.style.display = "";
phonenumberRemoveButton.style.display = "none";
} else {
phonenumberVerifyButton.style.display = "none";
phonenumberRemoveButton.style.display = "none";
}
}
else {
phonenumberVerifyButton.style.display = "";
phonenumberRemoveButton.style.display = "none";
}
phoneInputField.addEventListener("keyup", () => {
if (iti.isValidNumber() == false) {
phonenumberVerifyButton.style.display = "none";
phonenumberRemoveButton.style.display = "none";
} else {
phonenumberVerifyButton.style.display = "";
phonenumberRemoveButton.style.display = "none";
}
})
phonenumberRemoveButton.addEventListener("click", () => {
fetch('/api/config/phone', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.status == "ok") {
phonenumberVerifyButton.style.display = "";
phonenumberRemoveButton.style.display = "none";
phonenumberVerifiedText.style.display = "none";
phonenumberUnverifiedText.style.display = "block";
}
})
})
phonenumberVerifyButton.addEventListener("click", () => {
console.log(iti.getValidationError());
if (iti.isValidNumber() == false) {
phoneNumberUpdateCallback.innerHTML = "Invalid phone number: " + errorMap[iti.getValidationError()];
phoneNumberUpdateCallback.style.display = "block";
setTimeout(function() {
phoneNumberUpdateCallback.style.display = "none";
}, 5000);
} else {
alert("Valid number");
const mobileNumber = iti.getNumber();
fetch('/api/config/phone?phone_number=' + mobileNumber, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.status == "ok") {
if (isTwilioEnabled == "True" || isTwilioEnabled == "true") {
phoneNumberUpdateCallback.innerHTML = "OTP sent to your phone number";
phonenumberVerifyOTPButton.style.display = "block";
phonenumberOTPInput.style.display = "block";
} else {
phonenumberVerifiedText.style.display = "block";
phoneNumberUpdateCallback.innerHTML = "Phone number updated";
phonenumberUnverifiedText.style.display = "none";
}
phonenumberVerifyButton.style.display = "none";
phoneNumberUpdateCallback.style.display = "block";
setTimeout(function() {
phoneNumberUpdateCallback.style.display = "none";
}, 5000);
} else {
phoneNumberUpdateCallback.innerHTML = "Error updating phone number";
phoneNumberUpdateCallback.style.display = "block";
setTimeout(function() {
phoneNumberUpdateCallback.style.display = "none";
}, 5000);
}
})
.catch((error) => {
console.error('Error:', error);
phoneNumberUpdateCallback.innerHTML = "Error updating phone number";
phoneNumberUpdateCallback.style.display = "block";
setTimeout(function() {
phoneNumberUpdateCallback.style.display = "none";
}, 5000);
});
}
})
phonenumberVerifyOTPButton.addEventListener("click", () => {
const otp = phonenumberOTPInput.value;
if (otp.length != 6) {
phoneNumberUpdateCallback.innerHTML = "Your OTP should be exactly 6 digits";
phoneNumberUpdateCallback.style.display = "block";
setTimeout(function() {
phoneNumberUpdateCallback.style.display = "none";
}, 5000);
return;
}
fetch('/api/config/phone/verify?code=' + otp, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.status == "ok") {
phoneNumberUpdateCallback.innerHTML = "Phone number updated";
phonenumberVerifiedText.style.display = "block";
phonenumberUnverifiedText.style.display = "none";
phoneNumberUpdateCallback.style.display = "block";
phonenumberRemoveButton.style.display = "";
phonenumberVerifyButton.style.display = "none";
phonenumberVerifyOTPButton.style.display = "none";
phonenumberOTPInput.style.display = "none";
setTimeout(function() {
phoneNumberUpdateCallback.style.display = "none";
}, 5000);
} else {
phoneNumberUpdateCallback.innerHTML = "Error updating phone number";
phoneNumberUpdateCallback.style.display = "block";
setTimeout(function() {
phoneNumberUpdateCallback.style.display = "none";
}, 5000);
}
})
.catch((error) => {
console.error('Error:', error);
phoneNumberUpdateCallback.innerHTML = "Error updating phone number";
phoneNumberUpdateCallback.style.display = "block";
setTimeout(function() {
phoneNumberUpdateCallback.style.display = "none";
}, 5000);
});
})
</script> </script>
{% endblock %} {% endblock %}

View file

@ -10,7 +10,7 @@ from langchain.schema import ChatMessage
from transformers import AutoTokenizer from transformers import AutoTokenizer
from khoj.database.adapters import ConversationAdapters from khoj.database.adapters import ConversationAdapters
from khoj.database.models import KhojUser from khoj.database.models import ClientApplication, KhojUser
from khoj.utils.helpers import merge_dicts from khoj.utils.helpers import merge_dicts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -98,6 +98,7 @@ def save_to_conversation_log(
online_results: Dict[str, Any] = {}, online_results: Dict[str, Any] = {},
inferred_queries: List[str] = [], inferred_queries: List[str] = [],
intent_type: str = "remember", intent_type: str = "remember",
client_application: ClientApplication = None,
): ):
user_message_time = user_message_time or datetime.now().strftime("%Y-%m-%d %H:%M:%S") user_message_time = user_message_time or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
updated_conversation = message_to_log( updated_conversation = message_to_log(
@ -111,7 +112,7 @@ def save_to_conversation_log(
}, },
conversation_log=meta_log.get("chat", []), conversation_log=meta_log.get("chat", []),
) )
ConversationAdapters.save_conversation(user, {"chat": updated_conversation}) ConversationAdapters.save_conversation(user, {"chat": updated_conversation}, client_application=client_application)
def generate_chatml_messages_with_context( def generate_chatml_messages_with_context(

View file

@ -241,7 +241,9 @@ def chat_history(
validate_conversation_config() validate_conversation_config()
# Load Conversation History # Load Conversation History
meta_log = ConversationAdapters.get_conversation_by_user(user=user).conversation_log meta_log = ConversationAdapters.get_conversation_by_user(
user=user, client_application=request.user.client_app
).conversation_log
update_telemetry_state( update_telemetry_state(
request=request, request=request,
@ -424,7 +426,13 @@ async def chat(
} }
return Response(content=json.dumps(content_obj), media_type="application/json", status_code=status_code) return Response(content=json.dumps(content_obj), media_type="application/json", status_code=status_code)
await sync_to_async(save_to_conversation_log)( await sync_to_async(save_to_conversation_log)(
q, image, user, meta_log, intent_type="text-to-image", inferred_queries=[improved_image_prompt] q,
image,
user,
meta_log,
intent_type="text-to-image",
inferred_queries=[improved_image_prompt],
client_application=request.user.client_app,
) )
content_obj = {"image": image, "intentType": "text-to-image", "inferredQueries": [improved_image_prompt]} # type: ignore content_obj = {"image": image, "intentType": "text-to-image", "inferredQueries": [improved_image_prompt]} # type: ignore
return Response(content=json.dumps(content_obj), media_type="application/json", status_code=status_code) return Response(content=json.dumps(content_obj), media_type="application/json", status_code=status_code)
@ -438,6 +446,7 @@ async def chat(
inferred_queries, inferred_queries,
conversation_command, conversation_command,
user, user,
request.user.client_app,
) )
chat_metadata.update({"conversation_command": conversation_command.value}) chat_metadata.update({"conversation_command": conversation_command.value})

View file

@ -22,6 +22,7 @@ from khoj.database.models import (
NotionConfig, NotionConfig,
) )
from khoj.routers.helpers import CommonQueryParams, update_telemetry_state from khoj.routers.helpers import CommonQueryParams, update_telemetry_state
from khoj.routers.twilio import create_otp, is_twilio_enabled, verify_otp
from khoj.utils import constants, state from khoj.utils import constants, state
from khoj.utils.rawconfig import ( from khoj.utils.rawconfig import (
FullConfig, FullConfig,
@ -278,7 +279,77 @@ async def update_search_model(
return {"status": "ok"} return {"status": "ok"}
# Create Routes @api_config.post("/phone", status_code=200)
@requires(["authenticated"])
async def update_phone_number(
request: Request,
phone_number: str,
client: Optional[str] = None,
):
user = request.user.object
await adapters.aset_user_phone_number(user, phone_number)
if is_twilio_enabled():
create_otp(user)
else:
logger.warning("Phone verification is not enabled")
update_telemetry_state(
request=request,
telemetry_type="api",
api="set_phone_number",
client=client,
metadata={"phone_number": phone_number},
)
return {"status": "ok"}
@api_config.delete("/phone", status_code=200)
@requires(["authenticated"])
async def delete_phone_number(
request: Request,
client: Optional[str] = None,
):
user = request.user.object
await adapters.aremove_phone_number(user)
update_telemetry_state(
request=request,
telemetry_type="api",
api="delete_phone_number",
client=client,
)
return {"status": "ok"}
@api_config.post("/phone/verify", status_code=200)
@requires(["authenticated"])
async def verify_mobile_otp(
request: Request,
code: str,
client: Optional[str] = None,
):
user: KhojUser = request.user.object
update_telemetry_state(
request=request,
telemetry_type="api",
api="verify_phone_number",
client=client,
)
if is_twilio_enabled() and not verify_otp(user, code):
raise HTTPException(status_code=400, detail="Invalid OTP")
user.verified_phone_number = True
await user.asave()
return {"status": "ok"}
@api_config.get("/index/size", response_model=Dict[str, int]) @api_config.get("/index/size", response_model=Dict[str, int])
@requires(["authenticated"]) @requires(["authenticated"])
async def get_indexed_data_size(request: Request, common: CommonQueryParams): async def get_indexed_data_size(request: Request, common: CommonQueryParams):

View file

@ -208,6 +208,7 @@ def generate_chat_response(
inferred_queries: List[str] = [], inferred_queries: List[str] = [],
conversation_command: ConversationCommand = ConversationCommand.Default, conversation_command: ConversationCommand = ConversationCommand.Default,
user: KhojUser = None, user: KhojUser = None,
client_application: ClientApplication = None,
) -> Tuple[Union[ThreadedGenerator, Iterator[str]], Dict[str, str]]: ) -> Tuple[Union[ThreadedGenerator, Iterator[str]], Dict[str, str]]:
# Initialize Variables # Initialize Variables
chat_response = None chat_response = None
@ -224,6 +225,7 @@ def generate_chat_response(
compiled_references=compiled_references, compiled_references=compiled_references,
online_results=online_results, online_results=online_results,
inferred_queries=inferred_queries, inferred_queries=inferred_queries,
client_application=client_application,
) )
conversation_config = ConversationAdapters.get_valid_conversation_config(user) conversation_config = ConversationAdapters.get_valid_conversation_config(user)

View file

@ -0,0 +1,36 @@
import logging
import os
from twilio.rest import Client
from khoj.database.models import KhojUser
logger = logging.getLogger(__name__)
account_sid = os.getenv("TWILIO_ACCOUNT_SID")
auth_token = os.getenv("TWILIO_AUTH_TOKEN")
verification_service_sid = os.getenv("TWILIO_VERIFICATION_SID")
twilio_enabled = account_sid is not None and auth_token is not None and verification_service_sid is not None
if twilio_enabled:
client = Client(account_sid, auth_token)
def is_twilio_enabled():
return twilio_enabled
def create_otp(user: KhojUser):
"""Create a new OTP for the user"""
verification = client.verify.v2.services(verification_service_sid).verifications.create(
to=str(user.phone_number), channel="whatsapp"
)
return verification.sid is not None
def verify_otp(user: KhojUser, code: str):
"""Verify the OTP for the user"""
verification_check = client.verify.v2.services(verification_service_sid).verification_checks.create(
to=str(user.phone_number), code=code
)
return verification_check.status == "approved"

View file

@ -1,6 +1,5 @@
# System Packages # System Packages
import json import json
import math
import os import os
from datetime import timedelta from datetime import timedelta
@ -18,6 +17,7 @@ from khoj.database.adapters import (
get_user_subscription_state, get_user_subscription_state,
) )
from khoj.database.models import KhojUser from khoj.database.models import KhojUser
from khoj.routers.twilio import is_twilio_enabled
from khoj.utils import constants, state from khoj.utils import constants, state
from khoj.utils.rawconfig import ( from khoj.utils.rawconfig import (
GithubContentConfig, GithubContentConfig,
@ -174,6 +174,9 @@ def config_page(request: Request):
"khoj_cloud_subscription_url": os.getenv("KHOJ_CLOUD_SUBSCRIPTION_URL"), "khoj_cloud_subscription_url": os.getenv("KHOJ_CLOUD_SUBSCRIPTION_URL"),
"is_active": has_required_scope(request, ["premium"]), "is_active": has_required_scope(request, ["premium"]),
"has_documents": has_documents, "has_documents": has_documents,
"phone_number": user.phone_number,
"is_twilio_enabled": is_twilio_enabled(),
"is_phone_number_verified": user.verified_phone_number,
}, },
) )