Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfe2645a83 | ||
|
|
bbb51c52f6 |
BIN
ai/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
ai/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
ai/__pycache__/local_ai_api.cpython-311.pyc
Normal file
BIN
ai/__pycache__/local_ai_api.cpython-311.pyc
Normal file
Binary file not shown.
BIN
assets/pasted-20260207-143602-cafd6a6e.jpg
Normal file
BIN
assets/pasted-20260207-143602-cafd6a6e.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 185 KiB |
BIN
assets/pasted-20260207-144449-0aa0428f.jpg
Normal file
BIN
assets/pasted-20260207-144449-0aa0428f.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
BIN
assets/pasted-20260207-144713-9a0f04f5.jpg
Normal file
BIN
assets/pasted-20260207-144713-9a0f04f5.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
BIN
assets/pasted-20260207-145042-94187b95.jpg
Normal file
BIN
assets/pasted-20260207-145042-94187b95.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 220 KiB |
0
bridge.log
Normal file
0
bridge.log
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +1,11 @@
|
||||
from django.contrib import admin
|
||||
from .models import BotSettings, Message
|
||||
|
||||
# Register your models here.
|
||||
@admin.register(BotSettings)
|
||||
class BotSettingsAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'is_active', 'system_prompt')
|
||||
|
||||
@admin.register(Message)
|
||||
class MessageAdmin(admin.ModelAdmin):
|
||||
list_display = ('sender_number', 'message_in', 'message_out', 'timestamp')
|
||||
list_filter = ('timestamp', 'sender_number')
|
||||
33
core/migrations/0001_initial.py
Normal file
33
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-07 14:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BotSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('system_prompt', models.TextField(default='You are a helpful assistant.')),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('verify_token', models.CharField(default='my_secure_token_123', max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Message',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('sender_number', models.CharField(max_length=50)),
|
||||
('message_in', models.TextField()),
|
||||
('message_out', models.TextField(blank=True, null=True)),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-07 14:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='botsettings',
|
||||
name='whatsapp_access_token',
|
||||
field=models.CharField(blank=True, help_text='Meta Graph API Access Token', max_length=500, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='botsettings',
|
||||
name='whatsapp_phone_number_id',
|
||||
field=models.CharField(blank=True, help_text='WhatsApp Phone Number ID', max_length=50, null=True),
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +1,20 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
class BotSettings(models.Model):
|
||||
system_prompt = models.TextField(default="You are a helpful assistant.")
|
||||
is_active = models.BooleanField(default=True)
|
||||
verify_token = models.CharField(max_length=255, default="my_secure_token_123")
|
||||
whatsapp_access_token = models.CharField(max_length=500, blank=True, null=True, help_text="Meta Graph API Access Token")
|
||||
whatsapp_phone_number_id = models.CharField(max_length=50, blank=True, null=True, help_text="WhatsApp Phone Number ID")
|
||||
|
||||
def __str__(self):
|
||||
return f"Bot Settings (Active: {self.is_active})"
|
||||
|
||||
class Message(models.Model):
|
||||
sender_number = models.CharField(max_length=50)
|
||||
message_in = models.TextField()
|
||||
message_out = models.TextField(null=True, blank=True)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"From {self.sender_number} at {self.timestamp}"
|
||||
|
||||
@ -1,25 +1,105 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
<meta property="og:description" content="{{ project_description }}">
|
||||
<meta property="twitter:description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
{% if project_image_url %}
|
||||
<meta property="og:image" content="{{ project_image_url }}">
|
||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||
{% endif %}
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
{% block head %}{% endblock %}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}WhatsApp AI Bot{% endblock %}</title>
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Custom Styles -->
|
||||
<style>
|
||||
:root {
|
||||
--wa-green: #00A884;
|
||||
--wa-green-dark: #128C7E;
|
||||
--wa-teal: #075E54;
|
||||
--wa-bg: #F0F2F5;
|
||||
--wa-text: #111B21;
|
||||
}
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--wa-bg);
|
||||
color: var(--wa-text);
|
||||
}
|
||||
.navbar {
|
||||
background-color: var(--wa-teal);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.navbar-brand {
|
||||
font-weight: 700;
|
||||
color: white !important;
|
||||
}
|
||||
.nav-link {
|
||||
color: rgba(255,255,255,0.8) !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
.nav-link:hover {
|
||||
color: white !important;
|
||||
}
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: var(--wa-green);
|
||||
border-color: var(--wa-green);
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: var(--wa-green-dark);
|
||||
border-color: var(--wa-green-dark);
|
||||
}
|
||||
.status-badge {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.status-active {
|
||||
background-color: #dcf8c6;
|
||||
color: #075e54;
|
||||
}
|
||||
.status-inactive {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
</style>
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
<nav class="navbar navbar-expand-lg mb-4">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{% url 'index' %}">WhatsApp AI</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'index' %}">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'settings' %}">Bot Settings</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/" target="_blank">Admin Panel</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@ -1,145 +1,65 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ project_name }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes bg-pan {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
||||
font-weight: 700;
|
||||
margin: 0 0 1.2rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: 1.5rem auto;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.runtime code {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block title %}Dashboard - WhatsApp AI Bot{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your app…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<h2 class="fw-bold">Live Chat Monitor</h2>
|
||||
<p class="text-muted">Real-time view of incoming and AI-generated messages.</p>
|
||||
</div>
|
||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
||||
<p class="runtime">
|
||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||
</p>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="card p-3 d-inline-block">
|
||||
<span class="fw-bold me-2">Bot Status:</span>
|
||||
{% if settings.is_active %}
|
||||
<span class="status-badge status-active">Active</span>
|
||||
{% else %}
|
||||
<span class="status-badge status-inactive">Paused</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 150px;">Sender</th>
|
||||
<th>Message In</th>
|
||||
<th>AI Response</th>
|
||||
<th style="width: 180px;">Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for msg in messages %}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="badge bg-light text-dark p-2">{{ msg.sender_number }}</span>
|
||||
</td>
|
||||
<td>{{ msg.message_in }}</td>
|
||||
<td>
|
||||
{% if msg.message_out %}
|
||||
<div class="p-2 rounded bg-light" style="border-left: 4px solid var(--wa-green);">
|
||||
{{ msg.message_out }}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-muted italic">Processing...</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">{{ msg.timestamp|date:"M d, H:i:s" }}</small>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-5 text-muted">
|
||||
No messages received yet. Send a message to your WhatsApp number to see it here.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
131
core/templates/core/settings.html
Normal file
131
core/templates/core/settings.html
Normal file
@ -0,0 +1,131 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Bot Settings - WhatsApp AI Bot{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
|
||||
<!-- Pairing Mode Section -->
|
||||
<div class="card p-4 mb-4 border-primary shadow-sm">
|
||||
<h5 class="fw-bold text-primary mb-3"><i class="bi bi-phone me-2"></i>Link Personal WhatsApp</h5>
|
||||
<p class="small text-muted mb-3">
|
||||
Connect your existing WhatsApp account using the <b>Link Device</b> feature.
|
||||
Enter your number below to generate a pairing code.
|
||||
</p>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" id="pairingPhone" class="form-control" placeholder="Phone Number (e.g. 15551234567)">
|
||||
<button class="btn btn-dark" type="button" onclick="getPairingCode()">Get Pairing Code</button>
|
||||
</div>
|
||||
|
||||
<div id="pairingCodeDisplay" class="alert alert-success d-none text-center">
|
||||
<span class="text-muted small d-block mb-1">Enter this code on your phone (WhatsApp > Linked Devices > Link with phone number):</span>
|
||||
<h2 class="fw-bold font-monospace letter-spacing-2" id="codeValue" style="letter-spacing: 5px;">----</h2>
|
||||
</div>
|
||||
<div id="pairingError" class="alert alert-danger d-none small"></div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 shadow-sm">
|
||||
<h2 class="fw-bold mb-4">AI Bot Configuration</h2>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
|
||||
<h5 class="mb-3 text-secondary">🤖 Personality</h5>
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-600">System Instruction (Prompt)</label>
|
||||
<textarea name="system_prompt" class="form-control" rows="5" placeholder="Define how the AI should behave...">{{ settings.system_prompt }}</textarea>
|
||||
<div class="form-text mt-2">
|
||||
This instruction defines the bot's tone, personality, and knowledge limits.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5 class="mb-3 text-secondary">🔌 Meta Connection (Official API)</h5>
|
||||
<p class="small text-muted">Use these settings only if you are <b>not</b> using the pairing mode above.</p>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-600">Verification Token</label>
|
||||
<input type="text" name="verify_token" class="form-control" value="{{ settings.verify_token }}">
|
||||
<div class="form-text">
|
||||
Use this arbitrary string when verifying the webhook in the Meta Portal.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-600">Phone Number ID</label>
|
||||
<input type="text" name="whatsapp_phone_number_id" class="form-control" value="{{ settings.whatsapp_phone_number_id|default:'' }}" placeholder="e.g. 100609346...">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-600">Access Token</label>
|
||||
<input type="password" name="whatsapp_access_token" class="form-control" value="{{ settings.whatsapp_access_token|default:'' }}" placeholder="Meta Graph API Token">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch mb-4 mt-3">
|
||||
<input class="form-check-input" type="checkbox" name="is_active" id="activeSwitch" {% if settings.is_active %}checked{% endif %}>
|
||||
<label class="form-check-label fw-600" for="activeSwitch">Enable Auto-Reply</label>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'index' %}" class="btn btn-light">Back to Dashboard</a>
|
||||
<button type="submit" class="btn btn-primary px-4">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card mt-4 p-4 border-info bg-info bg-opacity-10">
|
||||
<h5><i class="bi bi-info-circle me-2"></i>Setup Tip</h5>
|
||||
<p class="mb-0 small text-dark">
|
||||
Your Webhook URL: <code>https://{{ request.get_host }}/webhook/whatsapp/</code><br>
|
||||
Verification Token: <code>{{ settings.verify_token }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function getPairingCode() {
|
||||
const btn = document.querySelector('button[onclick="getPairingCode()"]');
|
||||
const input = document.getElementById('pairingPhone');
|
||||
const display = document.getElementById('pairingCodeDisplay');
|
||||
const codeVal = document.getElementById('codeValue');
|
||||
const errorBox = document.getElementById('pairingError');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerText = "Requesting...";
|
||||
errorBox.classList.add('d-none');
|
||||
display.classList.add('d-none');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('phone_number', input.value);
|
||||
|
||||
try {
|
||||
const res = await fetch("{% url 'get_pairing_code' %}", {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
codeVal.innerText = data.code;
|
||||
display.classList.remove('d-none');
|
||||
// Reload page or update UI to show "Bridge Connected"?
|
||||
// For now just show the code.
|
||||
} else {
|
||||
errorBox.innerText = data.error || "Failed to get code";
|
||||
errorBox.classList.remove('d-none');
|
||||
}
|
||||
} catch (e) {
|
||||
errorBox.innerText = "Connection error. Ensure the internal Bridge Service is running.";
|
||||
errorBox.classList.remove('d-none');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerText = "Get Pairing Code";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -1,7 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import home
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
path('', views.index, name='index'),
|
||||
path('settings/', views.settings_view, name='settings'),
|
||||
path('settings/pair/', views.get_pairing_code, name='get_pairing_code'),
|
||||
path('webhook/whatsapp/', views.webhook, name='whatsapp_webhook'),
|
||||
]
|
||||
|
||||
202
core/views.py
202
core/views.py
@ -1,25 +1,187 @@
|
||||
import os
|
||||
import platform
|
||||
import json
|
||||
import logging
|
||||
import httpx
|
||||
from django.shortcuts import render, redirect
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from .models import Message, BotSettings
|
||||
from ai.local_ai_api import LocalAIApi
|
||||
|
||||
from django import get_version as django_version
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def home(request):
|
||||
"""Render the landing screen with loader and environment details."""
|
||||
host_name = request.get_host().lower()
|
||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||
now = timezone.now()
|
||||
def index(request):
|
||||
messages = Message.objects.all().order_by('-timestamp')[:50]
|
||||
settings = BotSettings.objects.first()
|
||||
if not settings:
|
||||
settings = BotSettings.objects.create()
|
||||
|
||||
context = {
|
||||
"project_name": "New Style",
|
||||
"agent_brand": agent_brand,
|
||||
"django_version": django_version(),
|
||||
"python_version": platform.python_version(),
|
||||
"current_time": now,
|
||||
"host_name": host_name,
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
'messages': messages,
|
||||
'settings': settings,
|
||||
}
|
||||
return render(request, "core/index.html", context)
|
||||
return render(request, 'core/index.html', context)
|
||||
|
||||
def settings_view(request):
|
||||
settings = BotSettings.objects.first()
|
||||
if not settings:
|
||||
settings = BotSettings.objects.create()
|
||||
|
||||
if request.method == 'POST':
|
||||
settings.system_prompt = request.POST.get('system_prompt', settings.system_prompt)
|
||||
settings.is_active = 'is_active' in request.POST
|
||||
settings.verify_token = request.POST.get('verify_token', settings.verify_token)
|
||||
settings.whatsapp_access_token = request.POST.get('whatsapp_access_token', settings.whatsapp_access_token)
|
||||
settings.whatsapp_phone_number_id = request.POST.get('whatsapp_phone_number_id', settings.whatsapp_phone_number_id)
|
||||
settings.save()
|
||||
return redirect('index')
|
||||
|
||||
return render(request, 'core/settings.html', {'settings': settings})
|
||||
|
||||
@csrf_exempt
|
||||
def get_pairing_code(request):
|
||||
if request.method != 'POST':
|
||||
return JsonResponse({'error': 'POST required'}, status=405)
|
||||
|
||||
phone_number = request.POST.get('phone_number')
|
||||
if not phone_number:
|
||||
return JsonResponse({'error': 'Phone number required'}, status=400)
|
||||
|
||||
# Call Bridge
|
||||
try:
|
||||
resp = httpx.post("http://127.0.0.1:3000/pair", json={"phoneNumber": phone_number}, timeout=30)
|
||||
if resp.status_code == 200:
|
||||
# Auto-configure settings for Bridge Mode
|
||||
settings = BotSettings.objects.first()
|
||||
if settings:
|
||||
settings.whatsapp_access_token = "BRIDGE"
|
||||
settings.whatsapp_phone_number_id = "BRIDGE"
|
||||
settings.save()
|
||||
return JsonResponse(resp.json())
|
||||
else:
|
||||
return JsonResponse(resp.json(), status=resp.status_code)
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': str(e)}, status=500)
|
||||
|
||||
@csrf_exempt
|
||||
def webhook(request):
|
||||
if request.method == 'GET':
|
||||
# Meta Webhook verification
|
||||
mode = request.GET.get('hub.mode')
|
||||
token = request.GET.get('hub.verify_token')
|
||||
challenge = request.GET.get('hub.challenge')
|
||||
|
||||
settings = BotSettings.objects.first()
|
||||
verify_token = settings.verify_token if settings else "my_secure_token_123"
|
||||
|
||||
if mode and token:
|
||||
if mode == 'subscribe' and token == verify_token:
|
||||
logger.info("WEBHOOK_VERIFIED")
|
||||
return HttpResponse(challenge)
|
||||
else:
|
||||
return HttpResponse('Verification failed', status=403)
|
||||
return HttpResponse('Verification failed', status=403)
|
||||
|
||||
elif request.method == 'POST':
|
||||
try:
|
||||
data = json.loads(request.body.decode('utf-8'))
|
||||
logger.info(f"Incoming WhatsApp data: {json.dumps(data)}")
|
||||
|
||||
# Check if it's a message from WhatsApp (Meta or Bridge)
|
||||
if 'object' in data and data['object'] == 'whatsapp_business_account':
|
||||
for entry in data.get('entry', []):
|
||||
for change in entry.get('changes', []):
|
||||
value = change.get('value', {})
|
||||
if 'messages' in value:
|
||||
for msg in value['messages']:
|
||||
sender_number = msg.get('from')
|
||||
message_body = msg.get('text', {}).get('body', '')
|
||||
|
||||
if message_body and sender_number:
|
||||
process_whatsapp_message(sender_number, message_body)
|
||||
|
||||
return JsonResponse({'status': 'ok'})
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing webhook: {str(e)}")
|
||||
return JsonResponse({'status': 'error', 'message': str(e)}, status=500)
|
||||
|
||||
def process_whatsapp_message(sender_number, message_body):
|
||||
settings = BotSettings.objects.first()
|
||||
if not settings or not settings.is_active:
|
||||
return
|
||||
|
||||
# Store incoming message
|
||||
db_msg = Message.objects.create(
|
||||
sender_number=sender_number,
|
||||
message_in=message_body
|
||||
)
|
||||
|
||||
# Call Gemini via AI Proxy
|
||||
prompt_input = [
|
||||
{"role": "system", "content": settings.system_prompt},
|
||||
{"role": "user", "content": message_body},
|
||||
]
|
||||
|
||||
try:
|
||||
response = LocalAIApi.create_response({
|
||||
"input": prompt_input,
|
||||
"model": "gemini-1.5-flash",
|
||||
})
|
||||
|
||||
if response.get("success"):
|
||||
ai_text = LocalAIApi.extract_text(response)
|
||||
db_msg.message_out = ai_text
|
||||
db_msg.save()
|
||||
|
||||
# Send response back to WhatsApp
|
||||
send_whatsapp_message(sender_number, ai_text, settings)
|
||||
|
||||
logger.info(f"Gemini response for {sender_number}: {ai_text}")
|
||||
else:
|
||||
logger.error(f"AI Proxy Error: {response.get('error')}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling Gemini: {str(e)}")
|
||||
|
||||
def send_whatsapp_message(to_number, text, settings):
|
||||
# Check for Bridge Mode
|
||||
if settings.whatsapp_access_token == "BRIDGE":
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.post(
|
||||
"http://127.0.0.1:3000/send",
|
||||
json={"to": to_number, "text": text},
|
||||
timeout=10
|
||||
)
|
||||
if response.status_code == 200:
|
||||
logger.info(f"Sent via Bridge to {to_number}")
|
||||
else:
|
||||
logger.error(f"Bridge Send Failed: {response.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"Bridge Exception: {e}")
|
||||
return
|
||||
|
||||
# Standard Meta Cloud API Mode
|
||||
if not settings.whatsapp_access_token or not settings.whatsapp_phone_number_id:
|
||||
logger.warning("WhatsApp credentials missing in settings. Cannot send reply.")
|
||||
return
|
||||
|
||||
url = f"https://graph.facebook.com/v17.0/{settings.whatsapp_phone_number_id}/messages"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {settings.whatsapp_access_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {
|
||||
"messaging_product": "whatsapp",
|
||||
"to": to_number,
|
||||
"type": "text",
|
||||
"text": {"body": text},
|
||||
}
|
||||
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.post(url, headers=headers, json=payload, timeout=10)
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Failed to send WhatsApp message: {response.text}")
|
||||
else:
|
||||
logger.info("WhatsApp message sent successfully.")
|
||||
except Exception as e:
|
||||
logger.error(f"Exception sending WhatsApp message: {str(e)}")
|
||||
2150
package-lock.json
generated
2150
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,11 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.8"
|
||||
"@whiskeysockets/baileys": "^7.0.0-rc.9",
|
||||
"body-parser": "^2.2.2",
|
||||
"bootstrap": "^5.3.8",
|
||||
"cors": "^2.8.6",
|
||||
"express": "^5.2.1",
|
||||
"pino": "^10.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
Django==5.2.7
|
||||
mysqlclient==2.2.7
|
||||
python-dotenv==1.1.1
|
||||
httpx==0.28.1
|
||||
|
||||
163
wa-bridge.js
Normal file
163
wa-bridge.js
Normal file
@ -0,0 +1,163 @@
|
||||
const { default: makeWASocket, useMultiFileAuthState, DisconnectReason } = require('@whiskeysockets/baileys');
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const pino = require('pino');
|
||||
const fs = require('fs');
|
||||
|
||||
const app = express();
|
||||
app.use(bodyParser.json());
|
||||
|
||||
const PORT = 3000;
|
||||
// We use 127.0.0.1 to be safe with localhost resolution
|
||||
const DJANGO_WEBHOOK = 'http://127.0.0.1:8000/webhook/whatsapp/';
|
||||
const AUTH_DIR = 'baileys_auth_info';
|
||||
|
||||
let sock;
|
||||
|
||||
async function connectToWhatsApp() {
|
||||
const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
|
||||
|
||||
sock = makeWASocket({
|
||||
logger: pino({ level: 'silent' }),
|
||||
printQRInTerminal: false,
|
||||
auth: state,
|
||||
browser: ["Gemini AI Bot", "Chrome", "3.0.0"],
|
||||
markOnlineOnConnect: true
|
||||
});
|
||||
|
||||
sock.ev.on('creds.update', saveCreds);
|
||||
|
||||
sock.ev.on('connection.update', (update) => {
|
||||
const { connection, lastDisconnect } = update;
|
||||
if(connection === 'close') {
|
||||
const shouldReconnect = (lastDisconnect?.error)?.output?.statusCode !== DisconnectReason.loggedOut;
|
||||
console.log('Connection closed. Reconnecting:', shouldReconnect);
|
||||
if(shouldReconnect) {
|
||||
connectToWhatsApp();
|
||||
} else {
|
||||
console.log('Logged out. Clearing session and restarting...');
|
||||
try {
|
||||
fs.rmSync(AUTH_DIR, { recursive: true, force: true });
|
||||
setTimeout(connectToWhatsApp, 2000); // Wait 2s before retry
|
||||
} catch (err) {
|
||||
console.error('Failed to reset session:', err);
|
||||
}
|
||||
}
|
||||
} else if(connection === 'open') {
|
||||
console.log('WhatsApp Connection Opened!');
|
||||
}
|
||||
});
|
||||
|
||||
sock.ev.on('messages.upsert', async m => {
|
||||
if(m.type === 'notify' || m.type === 'append') {
|
||||
for(const msg of m.messages) {
|
||||
if(!msg.message) continue;
|
||||
const key = msg.key;
|
||||
// Ignore messages from myself
|
||||
if (key.fromMe) continue;
|
||||
|
||||
const sender = key.remoteJid.split('@')[0];
|
||||
|
||||
// Extract text content from various message types
|
||||
let text = msg.message.conversation
|
||||
|| msg.message.extendedTextMessage?.text
|
||||
|| msg.message.imageMessage?.caption;
|
||||
|
||||
if(text) {
|
||||
console.log(`Received message from ${sender}: ${text}`);
|
||||
|
||||
// Construct payload matching Meta's Webhook format
|
||||
const payload = {
|
||||
object: 'whatsapp_business_account',
|
||||
entry: [{
|
||||
changes: [{
|
||||
value: {
|
||||
messages: [{
|
||||
from: sender,
|
||||
id: key.id,
|
||||
text: { body: text },
|
||||
type: 'text'
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}]
|
||||
};
|
||||
|
||||
try {
|
||||
// Use built-in fetch (Node 18+)
|
||||
const response = await fetch(DJANGO_WEBHOOK, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error(`Django webhook error: ${response.status}`);
|
||||
}
|
||||
} catch(err) {
|
||||
console.error("Failed to forward to Django:", err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start the socket logic
|
||||
connectToWhatsApp();
|
||||
|
||||
// --- API Endpoints ---
|
||||
|
||||
// 1. Request Pairing Code
|
||||
// Input: { phoneNumber: "15550001234" }
|
||||
app.post('/pair', async (req, res) => {
|
||||
let { phoneNumber } = req.body;
|
||||
if (!phoneNumber) return res.status(400).json({ error: 'Phone number required' });
|
||||
|
||||
// Sanitize: remove non-numeric chars (e.g. +62, 08-12, etc)
|
||||
phoneNumber = phoneNumber.toString().replace(/[^0-9]/g, '');
|
||||
|
||||
// Helper: If it starts with '08' (common Indonesian local format), replace leading '0' with '62'
|
||||
if (phoneNumber.startsWith('08')) {
|
||||
phoneNumber = '62' + phoneNumber.substring(1);
|
||||
}
|
||||
|
||||
if (!sock) {
|
||||
return res.status(503).json({ error: 'Socket not ready' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Wait a moment to ensure socket is ready for queries if we just restarted
|
||||
if (sock.authState.creds.registered) {
|
||||
return res.status(400).json({ error: "Already connected! Please delete session files to reset." });
|
||||
}
|
||||
|
||||
console.log(`Requesting pairing code for ${phoneNumber}...`);
|
||||
const code = await sock.requestPairingCode(phoneNumber);
|
||||
console.log(`Code generated: ${code}`);
|
||||
res.json({ code });
|
||||
} catch (e) {
|
||||
console.error("Pairing error:", e);
|
||||
res.status(500).json({ error: e.message || "Failed to generate pairing code" });
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Send Message (used by Django)
|
||||
// Input: { to: "15550001234", text: "Hello" }
|
||||
app.post('/send', async (req, res) => {
|
||||
const { to, text } = req.body;
|
||||
if (!to || !text) return res.status(400).json({ error: "Missing 'to' or 'text'" });
|
||||
|
||||
const jid = to.includes('@') ? to : `${to}@s.whatsapp.net`;
|
||||
|
||||
try {
|
||||
await sock.sendMessage(jid, { text: text });
|
||||
res.json({ status: 'sent' });
|
||||
} catch (e) {
|
||||
console.error("Send error:", e);
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`WhatsApp Bridge listening on port ${PORT}`);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user