Hatta biraz daha gelişmişi şöyle :

Python kodu (app.py) :
import os, csv, time, random, sqlite3, threading
from datetime import datetime, date
from flask import Flask, request, redirect, Response
import requests
from dotenv import load_dotenv

load_dotenv()

app = Flask(__name__)

TOKEN = os.getenv("WHATSAPP_TOKEN")
PHONE_NUMBER_ID = os.getenv("PHONE_NUMBER_ID")
DAILY_LIMIT = int(os.getenv("DAILY_LIMIT", 40))
MIN_DELAY = int(os.getenv("MIN_DELAY", 90))
MAX_DELAY = int(os.getenv("MAX_DELAY", 240))

DB = "whatsapp_crm.db"


def db():
    conn = sqlite3.connect(DB)
    conn.row_factory = sqlite3.Row
    return conn


def init_db():
    conn = db()
    conn.executescript("""
    CREATE TABLE IF NOT EXISTS customers (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT,
        phone TEXT UNIQUE,
        opt_in INTEGER DEFAULT 1,
        blocked INTEGER DEFAULT 0,
        created_at TEXT
    );

    CREATE TABLE IF NOT EXISTS messages (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        phone TEXT,
        message TEXT,
        status TEXT,
        response TEXT,
        created_at TEXT
    );
    """)
    conn.commit()
    conn.close()


def today_sent_count():
    conn = db()
    today = date.today().isoformat()
    count = conn.execute(
        "SELECT COUNT(*) FROM messages WHERE status='sent' AND DATE(created_at)=?",
        (today,)
    ).fetchone()[0]
    conn.close()
    return count


def already_sent_recent(phone):
    conn = db()
    row = conn.execute(
        """
        SELECT id FROM messages
        WHERE phone=? AND status='sent'
        AND datetime(created_at) >= datetime('now', '-7 days')
        LIMIT 1
        """,
        (phone,)
    ).fetchone()
    conn.close()
    return row is not None


def send_whatsapp(phone, message):
    url = f"https://graph.facebook.com/v20.0/{PHONE_NUMBER_ID}/messages"

    payload = {
        "messaging_product": "whatsapp",
        "to": phone,
        "type": "text",
        "text": {
            "preview_url": False,
            "body": message
        }
    }

    headers = {
        "Authorization": f"Bearer {TOKEN}",
        "Content-Type": "application/json"
    }

    r = requests.post(url, json=payload, headers=headers, timeout=30)
    return r.status_code, r.text


def log_message(phone, message, status, response):
    conn = db()
    conn.execute(
        """
        INSERT INTO messages(phone, message, status, response, created_at)
        VALUES (?, ?, ?, ?, ?)
        """,
        (phone, message, status, response, datetime.now().isoformat())
    )
    conn.commit()
    conn.close()


def campaign_worker(message_template):
    conn = db()
    customers = conn.execute(
        """
        SELECT * FROM customers
        WHERE opt_in=1 AND blocked=0
        ORDER BY id ASC
        """
    ).fetchall()
    conn.close()

    for c in customers:
        if today_sent_count() >= DAILY_LIMIT:
            break

        phone = c["phone"]
        name = c["name"] or ""

        if already_sent_recent(phone):
            continue

        message = message_template.replace("{name}", name).strip()
        message += "\n\nMesaj almak istemiyorsanız DUR yazabilirsiniz."

        code, resp = send_whatsapp(phone, message)

        if 200 <= code < 300:
            log_message(phone, message, "sent", resp)
        else:
            log_message(phone, message, "failed", resp)

        time.sleep(random.randint(MIN_DELAY, MAX_DELAY))


@app.route("/")
def index():
    conn = db()
    customers = conn.execute("SELECT * FROM customers ORDER BY id DESC LIMIT 100").fetchall()
    sent = today_sent_count()
    conn.close()

    rows = "".join(
        f"<tr><td>{c['id']}</td><td>{c['name']}</td><td>{c['phone']}</td><td>{c['opt_in']}</td><td>{c['blocked']}</td></tr>"
        for c in customers
    )

    return f"""
    <h2>WhatsApp CRM Panel</h2>

    <p>Bugün gönderilen: <b>{sent}/{DAILY_LIMIT}</b></p>

    <h3>Tek müşteri ekle</h3>
    <form method="post" action="/add">
        <input name="name" placeholder="İsim">
        <input name="phone" placeholder="905xxxxxxxxx" required>
        <button>Ekle</button>
    </form>

    <h3>CSV Import</h3>
    <p>CSV kolonları: name, phone, opt_in</p>
    <form method="post" action="/import" enctype="multipart/form-data">
        <input type="file" name="file" required>
        <button>İçe Aktar</button>
    </form>

    <h3>Kampanya gönder</h3>
    <form method="post" action="/campaign">
        <textarea name="message" rows="6" cols="70" placeholder="Merhaba {{name}}, kampanya mesajınız..." required></textarea><br>
        <button>Yavaş Gönderimi Başlat</button>
    </form>

    <p>
        <a href="/export/customers">Müşterileri CSV indir</a> |
        <a href="/export/messages">Mesaj loglarını CSV indir</a>
    </p>

    <h3>Son müşteriler</h3>
    <table border="1" cellpadding="6">
        <tr><th>ID</th><th>İsim</th><th>Telefon</th><th>Opt-in</th><th>Blocked</th></tr>
        {rows}
    </table>
    """


@app.route("/add", methods=["POST"])
def add():
    name = request.form.get("name", "").strip()
    phone = request.form.get("phone", "").strip()

    conn = db()
    conn.execute(
        """
        INSERT OR IGNORE INTO customers(name, phone, opt_in, blocked, created_at)
        VALUES (?, ?, 1, 0, ?)
        """,
        (name, phone, datetime.now().isoformat())
    )
    conn.commit()
    conn.close()

    return redirect("/")


@app.route("/import", methods=["POST"])
def import_csv():
    file = request.files["file"]
    content = file.stream.read().decode("utf-8-sig").splitlines()
    reader = csv.DictReader(content)

    conn = db()

    for row in reader:
        name = row.get("name", "").strip()
        phone = row.get("phone", "").strip()
        opt_in = 1 if row.get("opt_in", "yes").lower() in ["yes", "1", "true", "evet"] else 0

        if phone:
            conn.execute(
                """
                INSERT OR IGNORE INTO customers(name, phone, opt_in, blocked, created_at)
                VALUES (?, ?, ?, 0, ?)
                """,
                (name, phone, opt_in, datetime.now().isoformat())
            )

    conn.commit()
    conn.close()

    return redirect("/")


@app.route("/campaign", methods=["POST"])
def campaign():
    message = request.form["message"]

    t = threading.Thread(target=campaign_worker, args=(message,))
    t.daemon = True
    t.start()

    return "Kampanya arka planda başladı. Panelden logları export edebilirsin. <a href='/'>Geri dön</a>"


@app.route("/export/customers")
def export_customers():
    conn = db()
    rows = conn.execute("SELECT name, phone, opt_in, blocked, created_at FROM customers").fetchall()
    conn.close()

    def generate():
        yield "name,phone,opt_in,blocked,created_at\n"
        for r in rows:
            yield f"{r['name']},{r['phone']},{r['opt_in']},{r['blocked']},{r['created_at']}\n"

    return Response(generate(), mimetype="text/csv",
                    headers={"Content-Disposition": "attachment; filename=customers.csv"})


@app.route("/export/messages")
def export_messages():
    conn = db()
    rows = conn.execute("SELECT phone, message, status, response, created_at FROM messages").fetchall()
    conn.close()

    def generate():
        yield "phone,message,status,response,created_at\n"
        for r in rows:
            msg = str(r["message"]).replace('"', '""')
            resp = str(r["response"]).replace('"', '""')
            yield f'{r["phone"]},"{msg}",{r["status"]},"{resp}",{r["created_at"]}\n'

    return Response(generate(), mimetype="text/csv",
                    headers={"Content-Disposition": "attachment; filename=messages.csv"})


@app.route("/webhook", methods=["GET", "POST"])
def webhook():
    if request.method == "GET":
        verify_token = os.getenv("VERIFY_TOKEN")
        if request.args.get("hub.verify_token") == verify_token:
            return request.args.get("hub.challenge")
        return "Invalid verify token", 403

    data = request.json or {}

    try:
        message = data["entry"][0]["changes"][0]["value"]["messages"][0]
        phone = message["from"]
        text = message.get("text", {}).get("body", "").strip().lower()

        if text in ["dur", "stop", "iptal", "çık", "cik"]:
            conn = db()
            conn.execute("UPDATE customers SET blocked=1, opt_in=0 WHERE phone=?", (phone,))
            conn.commit()
            conn.close()

            send_whatsapp(phone, "Talebiniz alınmıştır. Bu numaradan artık bilgilendirme mesajı almayacaksınız.")
    except Exception:
        pass

    return "ok"


if __name__ == "__main__":
    init_db()
    app.run(host="0.0.0.0", port=5000, debug=True)


pip:

pip install flask requests python-dotenv

.env:

WHATSAPP_TOKEN=EAAG...
PHONE_NUMBER_ID=123456789
VERIFY_TOKEN=benim_verify_tokenim
DAILY_LIMIT=40
MIN_DELAY=90
MAX_DELAY=240



Çalıştır:

python app.py

Panel:

http://localhost:5000

bu bizim aktif kullandığımız.