Autosave: 20260207-145644

This commit is contained in:
Flatlogic Bot 2026-02-07 14:56:44 +00:00
parent bbb51c52f6
commit bfe2645a83
18 changed files with 2519 additions and 22 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

0
bridge.log Normal file
View File

View File

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

View File

@ -4,6 +4,8 @@ class BotSettings(models.Model):
system_prompt = models.TextField(default="You are a helpful assistant.") system_prompt = models.TextField(default="You are a helpful assistant.")
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
verify_token = models.CharField(max_length=255, default="my_secure_token_123") 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): def __str__(self):
return f"Bot Settings (Active: {self.is_active})" return f"Bot Settings (Active: {self.is_active})"

View File

@ -5,27 +5,65 @@
{% block content %} {% block content %}
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-8"> <div class="col-md-8">
<div class="card p-4">
<h2 class="fw-bold mb-4">AI Bot Personality</h2> <!-- 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"> <form method="POST">
{% csrf_token %} {% csrf_token %}
<h5 class="mb-3 text-secondary">🤖 Personality</h5>
<div class="mb-4"> <div class="mb-4">
<label class="form-label fw-600">System Instruction (Prompt)</label> <label class="form-label fw-600">System Instruction (Prompt)</label>
<textarea name="system_prompt" class="form-control" rows="6" placeholder="Define how the AI should behave...">{{ settings.system_prompt }}</textarea> <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"> <div class="form-text mt-2">
This instruction defines the bot's tone, personality, and knowledge limits. This instruction defines the bot's tone, personality, and knowledge limits.
</div> </div>
</div> </div>
<div class="mb-4"> <hr class="my-4">
<label class="form-label fw-600">Verification Token (Meta Webhook)</label>
<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 }}"> <input type="text" name="verify_token" class="form-control" value="{{ settings.verify_token }}">
<div class="form-text"> <div class="form-text">
Use this token when setting up the webhook in the Meta Developer Portal. Use this arbitrary string when verifying the webhook in the Meta Portal.
</div> </div>
</div> </div>
<div class="form-check form-switch mb-4"> <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 %}> <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> <label class="form-check-label fw-600" for="activeSwitch">Enable Auto-Reply</label>
</div> </div>
@ -42,10 +80,52 @@
<div class="card mt-4 p-4 border-info bg-info bg-opacity-10"> <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> <h5><i class="bi bi-info-circle me-2"></i>Setup Tip</h5>
<p class="mb-0 small text-dark"> <p class="mb-0 small text-dark">
Your Meta Webhook URL: <code>https://your-domain.com/webhook/whatsapp/</code><br> Your Webhook URL: <code>https://{{ request.get_host }}/webhook/whatsapp/</code><br>
Verification Token: <code>{{ settings.verify_token }}</code> Verification Token: <code>{{ settings.verify_token }}</code>
</p> </p>
</div> </div>
</div> </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 %} {% endblock %}

View File

@ -4,5 +4,6 @@ from . import views
urlpatterns = [ urlpatterns = [
path('', views.index, name='index'), path('', views.index, name='index'),
path('settings/', views.settings_view, name='settings'), 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'), path('webhook/whatsapp/', views.webhook, name='whatsapp_webhook'),
] ]

View File

@ -1,5 +1,6 @@
import json import json
import logging import logging
import httpx
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@ -29,11 +30,38 @@ def settings_view(request):
settings.system_prompt = request.POST.get('system_prompt', settings.system_prompt) settings.system_prompt = request.POST.get('system_prompt', settings.system_prompt)
settings.is_active = 'is_active' in request.POST settings.is_active = 'is_active' in request.POST
settings.verify_token = request.POST.get('verify_token', settings.verify_token) 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() settings.save()
return redirect('index') return redirect('index')
return render(request, 'core/settings.html', {'settings': settings}) 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 @csrf_exempt
def webhook(request): def webhook(request):
if request.method == 'GET': if request.method == 'GET':
@ -58,17 +86,17 @@ def webhook(request):
data = json.loads(request.body.decode('utf-8')) data = json.loads(request.body.decode('utf-8'))
logger.info(f"Incoming WhatsApp data: {json.dumps(data)}") logger.info(f"Incoming WhatsApp data: {json.dumps(data)}")
# Check if it's a message from WhatsApp # Check if it's a message from WhatsApp (Meta or Bridge)
if 'object' in data and data['object'] == 'whatsapp_business_account': if 'object' in data and data['object'] == 'whatsapp_business_account':
for entry in data['entry']: for entry in data.get('entry', []):
for change in entry['changes']: for change in entry.get('changes', []):
value = change['value'] value = change.get('value', {})
if 'messages' in value: if 'messages' in value:
for msg in value['messages']: for msg in value['messages']:
sender_number = msg['from'] sender_number = msg.get('from')
message_body = msg.get('text', {}).get('body', '') message_body = msg.get('text', {}).get('body', '')
if message_body: if message_body and sender_number:
process_whatsapp_message(sender_number, message_body) process_whatsapp_message(sender_number, message_body)
return JsonResponse({'status': 'ok'}) return JsonResponse({'status': 'ok'})
@ -96,7 +124,7 @@ def process_whatsapp_message(sender_number, message_body):
try: try:
response = LocalAIApi.create_response({ response = LocalAIApi.create_response({
"input": prompt_input, "input": prompt_input,
"model": "gemini-1.5-flash", # Explicitly request gemini-1.5-flash "model": "gemini-1.5-flash",
}) })
if response.get("success"): if response.get("success"):
@ -104,10 +132,56 @@ def process_whatsapp_message(sender_number, message_body):
db_msg.message_out = ai_text db_msg.message_out = ai_text
db_msg.save() db_msg.save()
# NOTE: In a real app, you'd call Meta API here to send db_msg.message_out back to sender_number. # Send response back to WhatsApp
# For this task, we focus on the integration and dashboard as requested. send_whatsapp_message(sender_number, ai_text, settings)
logger.info(f"Gemini response for {sender_number}: {ai_text}") logger.info(f"Gemini response for {sender_number}: {ai_text}")
else: else:
logger.error(f"AI Proxy Error: {response.get('error')}") logger.error(f"AI Proxy Error: {response.get('error')}")
except Exception as e: except Exception as e:
logger.error(f"Error calling Gemini: {str(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

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,11 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "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"
} }
} }

View File

@ -1,3 +1,4 @@
Django==5.2.7 Django==5.2.7
mysqlclient==2.2.7 mysqlclient==2.2.7
python-dotenv==1.1.1 python-dotenv==1.1.1
httpx==0.28.1

163
wa-bridge.js Normal file
View 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}`);
});