This commit is contained in:
2026-05-10 22:03:32 +07:00
parent b857b73132
commit 530b39c749
246 changed files with 27965 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
import { NextResponse } from "next/server";
import { loginWithQR } from "@/lib/zalo";
export async function POST() {
return new Promise((resolve) => {
loginWithQR((qr) => {
resolve(NextResponse.json({ qr }));
}).catch(err => {
console.error(err);
resolve(NextResponse.json({ error: "Failed to initialize login" }, { status: 500 }));
});
});
}
+7
View File
@@ -0,0 +1,7 @@
import { NextResponse } from "next/server";
import db from "@/lib/db";
export async function GET() {
const logs = db.prepare("SELECT * FROM messages ORDER BY timestamp DESC LIMIT 100").all();
return NextResponse.json({ logs });
}
+20
View File
@@ -0,0 +1,20 @@
import { NextResponse } from "next/server";
import db, { getSettings, saveSetting } from "@/lib/db";
export async function GET() {
return NextResponse.json({
smtp_host: getSettings("smtp_host") || "",
smtp_port: getSettings("smtp_port") || "465",
smtp_user: getSettings("smtp_user") || "",
smtp_pass: getSettings("smtp_pass") || "",
report_email: getSettings("report_email") || ""
});
}
export async function POST(req: Request) {
const data = await req.json();
for (const [key, value] of Object.entries(data)) {
saveSetting(key, String(value));
}
return NextResponse.json({ success: true });
}
+6
View File
@@ -0,0 +1,6 @@
import { NextResponse } from "next/server";
import { isConnected } from "@/lib/zalo";
export async function GET() {
return NextResponse.json({ connected: isConnected() });
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+26
View File
@@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
+36
View File
@@ -0,0 +1,36 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Zalo Monitor & Reporter",
description: "Real-time monitoring and daily reporting for Zalo messages",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
suppressHydrationWarning
>
<body className="min-h-full flex flex-col" suppressHydrationWarning>
{children}
</body>
</html>
);
}
+401
View File
@@ -0,0 +1,401 @@
"use client";
import { useState, useEffect } from "react";
import { Shield, MessageSquare, Settings, Mail, Bell, Play, Square, RefreshCcw, FileText } from "lucide-react";
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export default function Dashboard() {
const [activeTab, setActiveTab] = useState("monitor");
const [status, setStatus] = useState({ connected: false, loading: true });
const [qrCode, setQrCode] = useState<string | null>(null);
const [logs, setLogs] = useState<any[]>([]);
const [isLoggingIn, setIsLoggingIn] = useState(false);
useEffect(() => {
fetchStatus();
fetchLogs();
const interval = setInterval(fetchLogs, 5000);
return () => clearInterval(interval);
}, []);
const fetchStatus = async () => {
try {
const res = await fetch("/api/status");
const text = await res.text();
if (!res.ok) throw new Error(`Status error: ${res.status}`);
try {
const data = JSON.parse(text);
setStatus({ connected: data.connected, loading: false });
} catch (e) {
console.error("Status JSON Parse Error:", text);
throw new Error("Invalid status response");
}
} catch (e) {
console.error("Status Check Error:", e);
setStatus({ connected: false, loading: false });
}
};
const fetchLogs = async () => {
try {
const res = await fetch("/api/logs");
const text = await res.text();
if (!res.ok) throw new Error(`Logs error: ${res.status}`);
try {
const data = JSON.parse(text);
setLogs(data.logs || []);
} catch (e) {
console.error("Logs JSON Parse Error:", text);
}
} catch (e) {
console.error("Logs Fetch Error:", e);
}
};
const handleLogin = async () => {
if (isLoggingIn) return;
setIsLoggingIn(true);
setQrCode(null);
try {
const res = await fetch("/api/login", { method: "POST" });
const text = await res.text();
if (!res.ok) {
console.error("Server Error Body:", text);
throw new Error(`Server returned ${res.status}: ${res.statusText}`);
}
try {
const data = JSON.parse(text);
if (data.qr) {
setQrCode(data.qr);
}
} catch (parseError) {
console.error("JSON Parse Error. Body was:", text);
throw new Error("Invalid response format from server");
}
} catch (e) {
console.error("Login Error:", e);
alert(e instanceof Error ? e.message : "Failed to initialize login. Please try again.");
} finally {
setIsLoggingIn(false);
}
};
return (
<div className="min-h-screen bg-[#0a0a0c] text-slate-200 font-sans selection:bg-blue-500/30">
{/* Background Glow */}
<div className="fixed inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-[10%] -left-[10%] w-[40%] h-[40%] bg-blue-600/10 blur-[120px] rounded-full" />
<div className="absolute top-[60%] -right-[10%] w-[40%] h-[40%] bg-purple-600/10 blur-[120px] rounded-full" />
</div>
<div className="relative z-10 flex h-screen overflow-hidden">
{/* Sidebar */}
<aside className="w-64 border-r border-white/5 bg-black/20 backdrop-blur-xl flex flex-col">
<div className="p-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/20">
<Shield className="text-white w-6 h-6" />
</div>
<h1 className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-white to-slate-400">
Zalo Monitor
</h1>
</div>
</div>
<nav className="flex-1 px-4 py-4 space-y-2">
<NavItem
active={activeTab === "monitor"}
icon={<MessageSquare size={20} />}
label="Monitor"
onClick={() => setActiveTab("monitor")}
/>
<NavItem
active={activeTab === "logs"}
icon={<Bell size={20} />}
label="Activity Logs"
onClick={() => setActiveTab("logs")}
/>
<NavItem
active={activeTab === "settings"}
icon={<Settings size={20} />}
label="Configuration"
onClick={() => setActiveTab("settings")}
/>
</nav>
<div className="p-4 border-t border-white/5">
<div className="bg-white/5 rounded-2xl p-4 flex items-center gap-3">
<div className={cn(
"w-3 h-3 rounded-full animate-pulse",
status.connected ? "bg-green-500 shadow-[0_0_10px_rgba(34,197,94,0.5)]" : "bg-red-500"
)} />
<span className="text-sm font-medium">
{status.loading ? "Connecting..." : status.connected ? "Connected" : "Disconnected"}
</span>
</div>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 overflow-y-auto p-8">
<div className="max-w-5xl mx-auto">
{activeTab === "monitor" && (
<section className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<header className="flex justify-between items-end">
<div>
<h2 className="text-3xl font-bold text-white">System Monitor</h2>
<p className="text-slate-400 mt-1">Real-time surveillance and status control.</p>
</div>
{!status.connected && (
<button
onClick={handleLogin}
disabled={isLoggingIn}
className={cn(
"px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-semibold flex items-center gap-2 transition-all shadow-lg shadow-blue-600/20",
isLoggingIn ? "opacity-50 cursor-not-allowed" : "hover:scale-105 active:scale-95"
)}
>
{isLoggingIn ? <RefreshCcw className="animate-spin" size={18} /> : <Play size={18} />}
{isLoggingIn ? "Initializing..." : "Start Monitor"}
</button>
)}
</header>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<StatCard label="Messages Logged" value={logs.length.toString()} icon={<MessageSquare className="text-blue-400" />} />
<StatCard label="Last Report" value="Yesterday, 00:01" icon={<FileText className="text-purple-400" />} />
<StatCard label="Email Delivery" value="Active" icon={<Mail className="text-green-400" />} />
</div>
{!status.connected && qrCode && (
<div className="bg-white/5 border border-white/10 rounded-3xl p-8 flex flex-col items-center gap-6 backdrop-blur-md">
<h3 className="text-xl font-semibold">Scan QR Code to Login</h3>
<div className="p-4 bg-white rounded-2xl shadow-2xl">
<img src={qrCode} alt="Zalo QR" className="w-64 h-64" />
</div>
<p className="text-slate-400 text-sm max-w-md text-center">
Open Zalo on your phone, go to Menu {'>'} QR Code, and scan this code to link your account.
</p>
</div>
)}
<div className="bg-white/5 border border-white/10 rounded-3xl overflow-hidden backdrop-blur-md">
<div className="p-6 border-b border-white/10 flex justify-between items-center">
<h3 className="font-semibold text-lg">Recent Messages</h3>
<button onClick={fetchLogs} className="p-2 hover:bg-white/5 rounded-lg transition-colors">
<RefreshCcw size={18} className="text-slate-400" />
</button>
</div>
<div className="divide-y divide-white/5">
{logs.slice(0, 5).map((log, i) => (
<div key={i} className="p-4 flex items-start gap-4 hover:bg-white/[0.02] transition-colors">
<div className="w-10 h-10 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-400 font-bold shrink-0">
{log.senderName?.[0] || "?"}
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-center mb-1">
<span className="font-medium text-slate-100">{log.senderName}</span>
<span className="text-xs text-slate-500">{new Date(log.timestamp).toLocaleTimeString()}</span>
</div>
<p className="text-slate-400 text-sm truncate">{log.content}</p>
</div>
</div>
))}
{logs.length === 0 && (
<div className="p-12 text-center text-slate-500">
No activity recorded yet.
</div>
)}
</div>
</div>
</section>
)}
{activeTab === "logs" && <ActivityLogs logs={logs} />}
{activeTab === "settings" && <SettingsPage />}
</div>
</main>
</div>
</div>
);
}
function NavItem({ icon, label, active, onClick }: any) {
return (
<button
onClick={onClick}
className={cn(
"w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200",
active
? "bg-blue-600/10 text-blue-400 border border-blue-500/20"
: "text-slate-400 hover:bg-white/5 hover:text-slate-200"
)}
>
{icon}
<span className="font-medium">{label}</span>
</button>
);
}
function StatCard({ label, value, icon }: any) {
return (
<div className="bg-white/5 border border-white/10 rounded-2xl p-6 backdrop-blur-md">
<div className="flex justify-between items-start mb-4">
<div className="p-3 bg-white/5 rounded-xl">{icon}</div>
</div>
<div className="text-2xl font-bold text-white">{value}</div>
<div className="text-sm text-slate-400 mt-1">{label}</div>
</div>
);
}
function ActivityLogs({ logs }: any) {
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<h2 className="text-3xl font-bold text-white">Activity Logs</h2>
<div className="bg-white/5 border border-white/10 rounded-3xl overflow-hidden backdrop-blur-md">
<table className="w-full text-left">
<thead className="bg-white/5 text-slate-400 text-sm uppercase tracking-wider">
<tr>
<th className="px-6 py-4 font-medium">Timestamp</th>
<th className="px-6 py-4 font-medium">Sender</th>
<th className="px-6 py-4 font-medium">Type</th>
<th className="px-6 py-4 font-medium">Content</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{logs.map((log: any, i: number) => (
<tr key={i} className="hover:bg-white/[0.02] transition-colors">
<td className="px-6 py-4 text-sm text-slate-400">
{new Date(log.timestamp).toLocaleString()}
</td>
<td className="px-6 py-4 font-medium text-slate-200">{log.senderName}</td>
<td className="px-6 py-4 text-sm">
<span className={cn(
"px-2 py-1 rounded-md text-xs font-bold",
log.threadType === "Group" ? "bg-purple-500/10 text-purple-400" : "bg-blue-500/10 text-blue-400"
)}>
{log.threadType}
</span>
</td>
<td className="px-6 py-4 text-slate-400 text-sm max-w-md truncate">
{log.content}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
function SettingsPage() {
const [settings, setSettings] = useState({
smtp_host: "",
smtp_port: "465",
smtp_user: "",
smtp_pass: "",
report_email: ""
});
useEffect(() => {
fetch("/api/settings").then(res => res.json()).then(data => setSettings(data));
}, []);
const handleSave = async (e: React.FormEvent) => {
e.preventDefault();
await fetch("/api/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(settings)
});
alert("Settings saved!");
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<h2 className="text-3xl font-bold text-white">Configuration</h2>
<form onSubmit={handleSave} className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white/5 border border-white/10 rounded-3xl p-8 space-y-6 backdrop-blur-md">
<h3 className="text-xl font-semibold flex items-center gap-2">
<Mail size={20} className="text-blue-400" />
Email Settings
</h3>
<div className="space-y-4">
<Input
label="Recipient Email"
value={settings.report_email}
onChange={(v: string) => setSettings({...settings, report_email: v})}
placeholder="your@email.com"
/>
<Input
label="SMTP Host"
value={settings.smtp_host}
onChange={(v: string) => setSettings({...settings, smtp_host: v})}
placeholder="smtp.gmail.com"
/>
<Input
label="SMTP Port"
value={settings.smtp_port}
onChange={(v: string) => setSettings({...settings, smtp_port: v})}
placeholder="465"
/>
</div>
</div>
<div className="bg-white/5 border border-white/10 rounded-3xl p-8 space-y-6 backdrop-blur-md">
<h3 className="text-xl font-semibold flex items-center gap-2">
<Shield size={20} className="text-purple-400" />
Authentication
</h3>
<div className="space-y-4">
<Input
label="SMTP User"
value={settings.smtp_user}
onChange={(v: string) => setSettings({...settings, smtp_user: v})}
placeholder="your@gmail.com"
/>
<Input
label="SMTP Password"
value={settings.smtp_pass}
onChange={(v: string) => setSettings({...settings, smtp_pass: v})}
type="password"
placeholder="App Password"
/>
</div>
<div className="pt-4">
<button
type="submit"
className="w-full py-4 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-500 hover:to-purple-500 text-white rounded-2xl font-bold transition-all shadow-lg shadow-blue-600/20"
>
Save Configuration
</button>
</div>
</div>
</form>
</div>
);
}
function Input({ label, value, onChange, placeholder, type = "text" }: any) {
return (
<div className="space-y-1.5">
<label className="text-sm font-medium text-slate-400 ml-1">{label}</label>
<input
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-slate-200 placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500/40 transition-all"
/>
</div>
);
}
+13
View File
@@ -0,0 +1,13 @@
export async function register() {
if (process.env.NEXT_RUNTIME === "node") {
const { initCron } = await import("./lib/cron");
const { getZaloApi } = await import("./lib/zalo");
initCron();
// Attempt to login if credentials exist
getZaloApi().catch(err => {
console.error("Initial Zalo login failed:", err);
});
}
}
+12
View File
@@ -0,0 +1,12 @@
import cron from "node-cron";
import { processDailyReport } from "./reporting";
export function initCron() {
// Run every day at 00:01 AM
cron.schedule("1 0 * * *", () => {
console.log("Running daily report job...");
processDailyReport();
});
console.log("Cron jobs initialized");
}
+106
View File
@@ -0,0 +1,106 @@
import Database from "better-sqlite3";
import path from "node:path";
import fs from "node:fs";
const DB_PATH = path.join(process.cwd(), "data/monitor.db");
// Ensure data directory exists
if (!fs.existsSync(path.join(process.cwd(), "data"))) {
fs.mkdirSync(path.join(process.cwd(), "data"));
}
const db = new Database(DB_PATH);
// Initialize schema
db.exec(`
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
threadId TEXT NOT NULL,
senderId TEXT NOT NULL,
senderName TEXT,
content TEXT,
timestamp INTEGER NOT NULL,
threadType TEXT NOT NULL,
isSelf INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS threads (
id TEXT PRIMARY KEY,
name TEXT,
type TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT
);
CREATE TABLE IF NOT EXISTS credentials (
id INTEGER PRIMARY KEY CHECK (id = 1),
cookie TEXT,
imei TEXT,
userAgent TEXT,
updatedAt INTEGER
);
`);
export default db;
export function saveMessage(message: any) {
const stmt = db.prepare(`
INSERT OR REPLACE INTO messages (id, threadId, senderId, senderName, content, timestamp, threadType, isSelf)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
let content = "[Unknown Content]";
try {
content = typeof message.data.content === "string"
? message.data.content
: JSON.stringify(message.data.content);
} catch (e) {
content = "[Complex Object Content]";
}
stmt.run(
message.data.msgId || `tmp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
message.threadId,
message.data.uidFrom,
message.data.senderName || message.data.uidFrom,
content,
message.data.timestamp || Date.now(),
message.type,
message.isSelf ? 1 : 0
);
}
export function getSettings(key: string): string | null {
const stmt = db.prepare("SELECT value FROM settings WHERE key = ?");
const row = stmt.get(key) as { value: string } | undefined;
return row ? row.value : null;
}
export function saveSetting(key: string, value: string) {
const stmt = db.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)");
stmt.run(key, value);
}
export function saveCredentials(cookie: any, imei: string, userAgent: string) {
const stmt = db.prepare(`
INSERT OR REPLACE INTO credentials (id, cookie, imei, userAgent, updatedAt)
VALUES (1, ?, ?, ?, ?)
`);
stmt.run(JSON.stringify(cookie), imei, userAgent, Date.now());
}
export function getCredentials() {
const stmt = db.prepare("SELECT * FROM credentials WHERE id = 1");
const row = stmt.get() as any;
if (row) {
return {
cookie: JSON.parse(row.cookie),
imei: row.imei,
userAgent: row.userAgent
};
}
return null;
}
+119
View File
@@ -0,0 +1,119 @@
import PDFDocument from "pdfkit";
import nodemailer from "nodemailer";
import db, { getSettings } from "./db";
import fs from "node:fs";
import path from "node:path";
export async function generateDailyReport(date: Date) {
const startOfDay = new Date(date);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(date);
endOfDay.setHours(23, 59, 59, 999);
const messages = db.prepare(`
SELECT * FROM messages
WHERE timestamp >= ? AND timestamp <= ?
ORDER BY timestamp ASC
`).all(startOfDay.getTime(), endOfDay.getTime()) as any[];
if (messages.length === 0) {
console.log("No messages to report for", date.toDateString());
return null;
}
const doc = new PDFDocument();
const fileName = `report-${date.toISOString().split("T")[0]}.pdf`;
const filePath = path.join(process.cwd(), "data", fileName);
const stream = fs.createWriteStream(filePath);
doc.pipe(stream);
// Title
doc.fontSize(20).text(`Zalo Daily Report - ${date.toDateString()}`, { align: "center" });
doc.moveDown();
// Group by thread
const threads: Record<string, any[]> = {};
messages.forEach(msg => {
if (!threads[msg.threadId]) threads[msg.threadId] = [];
threads[msg.threadId].push(msg);
});
for (const [threadId, msgs] of Object.entries(threads)) {
const threadName = msgs[0].threadType === "Group" ? `Group: ${threadId}` : `User: ${msgs[0].senderName}`;
doc.fontSize(16).text(threadName, { underline: true });
doc.moveDown(0.5);
msgs.forEach(msg => {
const time = new Date(msg.timestamp).toLocaleTimeString();
doc.fontSize(10).text(`[${time}] ${msg.senderName}:`, { continued: true });
doc.fontSize(11).text(` ${msg.content}`);
});
doc.moveDown();
}
doc.end();
return new Promise<string>((resolve) => {
stream.on("finish", () => resolve(filePath));
});
}
export async function sendEmail(filePath: string, recipient: string) {
const smtpHost = getSettings("smtp_host") || "smtp.gmail.com";
const smtpPort = parseInt(getSettings("smtp_port") || "465");
const smtpUser = getSettings("smtp_user");
const smtpPass = getSettings("smtp_pass");
if (!smtpUser || !smtpPass) {
throw new Error("SMTP credentials not configured");
}
const transporter = nodemailer.createTransport({
host: smtpHost,
port: smtpPort,
secure: smtpPort === 465,
auth: {
user: smtpUser,
pass: smtpPass,
},
});
const info = await transporter.sendMail({
from: `"Zalo Monitor" <${smtpUser}>`,
to: recipient,
subject: `Zalo Daily Report - ${new Date().toDateString()}`,
text: "Please find the attached daily report for your Zalo messages.",
attachments: [
{
filename: path.basename(filePath),
path: filePath,
},
],
});
console.log("Email sent: %s", info.messageId);
return info;
}
export async function processDailyReport() {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const recipient = getSettings("report_email");
if (!recipient) {
console.error("Report email not configured");
return;
}
try {
const filePath = await generateDailyReport(yesterday);
if (filePath) {
await sendEmail(filePath, recipient);
// Optional: delete file after sending
// fs.unlinkSync(filePath);
}
} catch (error) {
console.error("Failed to process daily report:", error);
}
}
+110
View File
@@ -0,0 +1,110 @@
import { Zalo, ThreadType, LoginQRCallbackEventType } from "zca-js";
import db, { saveMessage, saveCredentials, getCredentials } from "./db";
let zaloInstance: Zalo | null = null;
let apiInstance: any = null;
let isInitializing = false;
function getZaloInstance() {
if (!zaloInstance) {
zaloInstance = new Zalo();
}
return zaloInstance;
}
export async function getZaloApi() {
if (apiInstance) return apiInstance;
const creds = getCredentials();
if (creds) {
try {
console.log("Attempting to login with saved credentials...");
const zalo = getZaloInstance();
const api = await zalo.login(creds);
apiInstance = api;
setupListener(api);
return api;
} catch (error) {
console.error("Failed to login with saved credentials:", error);
}
}
return null;
}
export async function loginWithQR(onQrGenerated: (qrData: string) => void) {
if (isInitializing) {
console.log("Already initializing Zalo login...");
return;
}
isInitializing = true;
console.log("Initializing Zalo QR login...");
const zalo = getZaloInstance();
try {
console.log("Calling zalo.loginQR...");
const api = await zalo.loginQR({}, (qrData: any) => {
console.log("Zalo login event type:", qrData.type);
if (qrData.type === LoginQRCallbackEventType.QRCodeGenerated) {
console.log("QR Code generated successfully");
let image = qrData.data.image;
if (!image.startsWith("data:image")) {
image = `data:image/png;base64,${image}`;
}
onQrGenerated(image);
}
});
console.log("Zalo login scan completed successfully");
const context = api.getContext();
saveCredentials(
context.cookie.toJSON()?.cookies,
context.imei,
context.userAgent
);
apiInstance = api;
setupListener(api);
return api;
} catch (error) {
console.error("Zalo loginQR process failed:", error);
throw error;
} finally {
isInitializing = false;
}
}
function setupListener(api: any) {
api.listener.on("message", (message: any) => {
let contentStr = "[Unknown Content]";
try {
contentStr = typeof message.data.content === "string"
? message.data.content
: JSON.stringify(message.data.content);
} catch (e) {
contentStr = "[Complex Object Content]";
}
console.log(`Received message from ${message.data.senderName || message.data.uidFrom}:`, contentStr);
saveMessage(message);
});
api.listener.onConnected(() => {
console.log("Zalo listener connected");
});
api.listener.onClosed(() => {
console.log("Zalo listener closed");
apiInstance = null;
});
api.listener.onError((error: any) => {
console.error("Zalo listener error:", error);
});
api.listener.start();
}
export function isConnected() {
return !!apiInstance;
}