mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-27 17:35:07 +01:00
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:
parent
58bf917775
commit
679db51453
15 changed files with 478 additions and 13 deletions
28
documentation/docs/clients/whatsapp.md
Normal file
28
documentation/docs/clients/whatsapp.md
Normal 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.
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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",)
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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:
|
||||||
|
|
17
src/khoj/interface/web/assets/icons/whatsapp.svg
Normal file
17
src/khoj/interface/web/assets/icons/whatsapp.svg
Normal 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 |
|
@ -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;
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
36
src/khoj/routers/twilio.py
Normal file
36
src/khoj/routers/twilio.py
Normal 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"
|
|
@ -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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue