Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 0 additions & 40 deletions src/server/client/client.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
--active: #2a3942;
--accent: #00a884;
--danger: #ef5350;
--unread: #00a884;
--shadow: rgba(0,0,0,.4);
}
html, body {
Expand Down Expand Up @@ -79,13 +78,6 @@ html, body {
.chat-meta { display: flex; justify-content: space-between; align-items: baseline; }
.chat-preview { font-size: 13px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-time { font-size: 11px; color: var(--text-secondary); white-space: nowrap; }
.chat-unread {
background: var(--unread); color: #fff;
border-radius: 50%; min-width: 20px; height: 20px;
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 600;
position: absolute; top: -4px; right: -4px;
}

/* ── Device Actions on Hover ── */
.device-actions {
Expand Down Expand Up @@ -157,19 +149,6 @@ html, body {
.msg.incoming .msg-footer { justify-content: flex-start; }
.msg-check { color: #53bdeb; }
.msg-check.delivered { color: var(--text-secondary); }
.msg-image { max-width: 260px; max-height: 260px; border-radius: 6px; display: block; cursor: pointer; object-fit: cover; }
.msg-audio { width: 100%; }
.msg-video { max-width: 260px; max-height: 260px; border-radius: 6px; }
.msg-document {
display: flex; align-items: center; gap: 10px;
padding: 8px; background: rgba(255,255,255,.05); border-radius: 6px;
cursor: pointer;
}
.msg-document i { font-size: 28px; color: var(--accent); }
.msg-doc-info { flex: 1; min-width: 0; }
.msg-doc-name { font-size: 13px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-doc-size { font-size: 11px; color: var(--text-secondary); }
.msg-sticker { width: 160px; border-radius: 8px; }

/* ── Load More ── */
.load-more {
Expand Down Expand Up @@ -246,25 +225,6 @@ html, body {
.icon-btn:hover { background: var(--hover); color: var(--text); }
.icon-btn.primary { color: var(--accent); }

/* ── Attachment Menu ── */
.attach-menu {
position: absolute; bottom: 68px; left: 16px;
display: flex; gap: 12px; padding: 12px;
background: var(--panel-bg); border-radius: 10px;
box-shadow: 0 4px 12px var(--shadow);
z-index: 10;
}
.attach-item {
display: flex; flex-direction: column;
align-items: center; gap: 4px; cursor: pointer;
}
.attach-item i {
width: 48px; height: 48px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 22px; color: #fff;
}
.attach-item span { font-size: 11px; color: var(--text-secondary); }

/* ── Device Dashboard ── */
.device-cards { display: flex; flex-wrap: wrap; gap: 16px; padding: 24px; }
.device-card {
Expand Down
89 changes: 4 additions & 85 deletions src/server/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,8 @@
<span class="reply-close" onclick="cancelReply()">&times;</span>
</div>
<div class="input-area" style="position:relative">
<button class="icon-btn" onclick="toggleAttachMenu()"><i class="bi bi-paperclip"></i></button>
<input id="msgInput" placeholder="Type a message" onkeydown="if(event.key==='Enter')sendMsg()">
<button class="icon-btn primary" onclick="sendMsg()"><i class="bi bi-send"></i></button>
<div class="attach-menu" id="attachMenu" style="display:none">
<div class="attach-item" onclick="pickFile('image/*')"><i style="background:#ef5350"><i class="bi bi-image"></i></i><span>Image</span></div>
<div class="attach-item" onclick="pickFile('audio/*')"><i style="background:#ff9800"><i class="bi bi-music-note"></i></i><span>Audio</span></div>
<div class="attach-item" onclick="pickFile('video/*')"><i style="background:#4caf50"><i class="bi bi-camera-reels"></i></i><span>Video</span></div>
<div class="attach-item" onclick="pickFile('*/*')"><i style="background:#2196f3"><i class="bi bi-file-earmark"></i></i><span>Document</span></div>
</div>
<input type="file" id="fileInput" style="display:none" onchange="sendMedia(event)">
</div>
</div>
<div class="empty-state" id="emptyState">
Expand All @@ -65,10 +57,7 @@
</main>
</div>
<div class="toast-container" id="toastContainer"></div>
<div id="imageModal" style="display:none;position:fixed;inset:0;z-index:999;background:rgba(0,0,0,.85);cursor:zoom-out;align-items:center;justify-content:center" onclick="this.style.display='none'">
<img id="modalImage" style="max-width:90vw;max-height:90vh;border-radius:8px;object-fit:contain">
</div>
<div id="addDeviceModal" style="display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.7);align-items:center;justify-content:center" onclick="if(event.target===this)this.style.display='none'">
<div id="addDeviceModal" style="display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.7);align-items:center;justify-content:center" onclick="if(event.target===this)this.style.display='none'">
<div style="background:var(--panel-bg);border-radius:12px;padding:24px;width:420px;max-width:90vw;box-shadow:0 8px 32px rgba(0,0,0,.5)">
<h3 style="margin-bottom:16px;font-weight:600">Tambah Device Baru</h3>
<input id="newDeviceName" placeholder="Nama device (contoh: HP Sales)" style="width:100%;padding:10px 14px;border:none;border-radius:8px;background:var(--input-bg);color:var(--text);font-size:14px;outline:none;margin-bottom:10px">
Expand Down Expand Up @@ -110,17 +99,6 @@ <h3 style="margin-bottom:16px;font-weight:600"><i class="bi bi-info-circle"></i>
$('toastContainer').appendChild(t);
setTimeout(() => t.remove(), 3000);
}
function showImage(src) {
$('modalImage').src = src;
$('imageModal').style.display = 'flex';
}
function mediaError(el) {
el.style.display = 'none';
const div = document.createElement('div');
div.style.cssText = 'padding:8px;color:var(--text-secondary);font-style:italic;font-size:12px';
div.textContent = 'Media tidak tersedia';
el.parentElement.appendChild(div);
}

// ── Add Device Modal ──
function showAddDeviceModal() {
Expand Down Expand Up @@ -537,10 +515,9 @@ <h3 style="margin-bottom:16px;font-weight:600"><i class="bi bi-info-circle"></i>
</div>
</div>`;
html += chats.map(c => {
const uc = c.unreadCount || 0;
const name = bestName(c);
return `<div class="chat-item${c.jid === currentJid ? ' active' : ''}" data-jid="${esc(c.jid)}" data-name="${esc(name.toLowerCase())}" onclick="selectChat('${c.jid}')">
<div class="chat-avatar">${esc(name[0] || '?')}${uc > 0 ? `<span class="chat-unread">${uc > 99 ? '99+' : uc}</span>` : ''}</div>
<div class="chat-avatar">${esc(name[0] || '?')}</div>
<div class="chat-info">
<div class="chat-meta">
<div class="chat-name">${esc(name)}</div>
Expand Down Expand Up @@ -651,33 +628,8 @@ <h3 style="margin-bottom:16px;font-weight:600"><i class="bi bi-info-circle"></i>
if (isFwd) {
html += '<div class="msg-forwarded"><i class="bi bi-forward"></i> Forwarded</div>';
}
// media content
if (content.type === 'image') {
const key = getMsgRemoteJid(m) + '-' + getMsgId(m);
html += `<img class="msg-image" src="/messages/media/${currentDevice}/${encodeURIComponent(key)}" alt="image" onerror="mediaError(this)" onclick="showImage(this.src)">`;
if (content.text && !content.text.startsWith('📷')) html += `<div class="msg-text">${esc(content.text)}</div>`;
} else if (content.type === 'video') {
const key = getMsgRemoteJid(m) + '-' + getMsgId(m);
html += `<video class="msg-video" src="/messages/media/${currentDevice}/${encodeURIComponent(key)}" controls onerror="mediaError(this)"></video>`;
if (content.text && !content.text.startsWith('🎬')) html += `<div class="msg-text">${esc(content.text)}</div>`;
} else if (content.type === 'audio') {
const key = getMsgRemoteJid(m) + '-' + getMsgId(m);
html += `<audio class="msg-audio" src="/messages/media/${currentDevice}/${encodeURIComponent(key)}" controls onerror="mediaError(this)"></audio>`;
} else if (content.type === 'document') {
const key = getMsgRemoteJid(m) + '-' + getMsgId(m);
html += `<div class="msg-document" onclick="window.open('/messages/media/${currentDevice}/${encodeURIComponent(key)}')">
<i class="bi bi-file-earmark"></i>
<div class="msg-doc-info">
<div class="msg-doc-name">${esc(content.obj?.fileName || 'Document')}</div>
<div class="msg-doc-size">${content.obj?.fileSize ? Math.round(content.obj.fileSize / 1024) + ' KB' : ''}</div>
</div>
</div>`;
} else if (content.type === 'sticker') {
const key = getMsgRemoteJid(m) + '-' + getMsgId(m);
html += `<img class="msg-sticker" src="/messages/media/${currentDevice}/${encodeURIComponent(key)}" alt="sticker" onerror="this.style.display='none'">`;
} else {
html += `<div class="msg-text">${esc(content.text)}</div>`;
}
// message content (media no longer supported)
html += `<div class="msg-text">${esc(content.text)}</div>`;
// footer
html += '<div class="msg-footer">';
html += `<span>${time}</span>`;
Expand Down Expand Up @@ -756,39 +708,6 @@ <h3 style="margin-bottom:16px;font-weight:600"><i class="bi bi-info-circle"></i>
$('replyPreview').style.display = 'none';
}

// ── Send Media ──
function pickFile(accept) {
$('attachMenu').style.display = 'none';
const input = $('fileInput');
input.accept = accept;
input.click();
}

async function sendMedia(event) {
const file = event.target.files[0];
if (!file || !currentDevice || !currentJid) return;

const fd = new FormData();
fd.append('deviceId', currentDevice);
fd.append('recipient', currentJid);
fd.append('file', file);
if (replyTarget) fd.append('replyId', replyTarget.id);

try {
await api('/messages/send-media', { method: 'POST', body: fd });
cancelReply();
toast('Media sent');
fetchMessages();
loadChats();
} catch (e) { toast('Send media failed: ' + e.message, 'error'); }
event.target.value = '';
}

function toggleAttachMenu() {
const el = $('attachMenu');
el.style.display = el.style.display === 'none' ? 'flex' : 'none';
}

// ── SSE Live ──
let typingTimers = {};

Expand Down
10 changes: 6 additions & 4 deletions src/server/connections/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { DB_PATH } from '@/config';
import { logger } from '@/logger';
import { SessionManager } from '@/whatsapp/session-manager';
import { DB_PATH } from '@/config';
import { Elysia, t } from 'elysia';
import { stringify } from 'qs';
import { readdirSync, statSync, existsSync } from 'node:fs';
import { existsSync, readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { stringify } from 'qs';

const sessionManager = SessionManager.getInstance();
const refreshGroupsRateLimit = new Map<string, number>();
Expand All @@ -19,7 +19,9 @@ function getDirSize(dir: string): number {
const full = join(dir, entry.name);
if (entry.isDirectory()) size += getDirSize(full);
else if (entry.isFile()) {
try { size += statSync(full).size; } catch {}
try {
size += statSync(full).size;
} catch {}
}
}
} catch {}
Expand Down
82 changes: 39 additions & 43 deletions src/server/contacts/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { SessionManager } from '@/whatsapp';
import { SessionManager, messageStore } from '@/whatsapp';
import { Elysia, t } from 'elysia';

const manager = SessionManager.getInstance();

const getWA = (deviceId: string) => {
const whatsapp = manager.getSession(deviceId);
if (!whatsapp) throw new Error(`Device ${deviceId} not found, please connect first`);
if (!whatsapp)
throw new Error(`Device ${deviceId} not found, please connect first`);
return whatsapp;
};

Expand All @@ -15,48 +16,43 @@ export const contacts = new Elysia({
tags: ['Contacts'],
description: 'Endpoints to get contact information',
},
})
.get(
'/:deviceId',
({ params: { deviceId }, set }) => {
try {
const whatsapp = getWA(deviceId);
const dbQueries = whatsapp.getDbQueries();
}).get(
'/:deviceId',
({ params: { deviceId }, set }) => {
try {
const whatsapp = getWA(deviceId);
const dbQueries = whatsapp.getDbQueries();

if (!dbQueries) {
set.status = 400;
return { success: false, message: 'Database not initialized' };
}
const chats = messageStore.listChatJids(deviceId);

const chats = dbQueries.listChatJids();
const unreads = dbQueries.getAllUnreadCounts();
const contacts = chats.map((chat) => {
const contact = dbQueries?.getContact(chat.chatJid);
return {
jid: chat.chatJid,
name: contact?.name ?? chat.chatJid.split('@')[0],
photo_url: contact?.photo_url ?? null,
lastMessage: chat.lastMessage,
lastTimestamp: chat.lastTimestamp,
messageCount: chat.count,
};
});

const contacts = chats.map((chat) => {
const contact = dbQueries.getContact(chat.chatJid);
return {
jid: chat.chatJid,
name: contact?.name ?? chat.chatJid.split('@')[0],
photo_url: contact?.photo_url ?? null,
lastMessage: chat.lastMessage,
lastTimestamp: chat.lastTimestamp,
messageCount: chat.count,
unreadCount: unreads[chat.chatJid] ?? 0,
};
});

return { success: true, data: contacts };
} catch (error) {
set.status = 400;
return { success: false, message: error instanceof Error ? error.message : String(error) };
}
},
{
params: t.Object({
deviceId: t.String(),
}),
detail: {
summary: 'List Contacts',
description: 'Get all contacts with last message and unread count.',
},
return { success: true, data: contacts };
} catch (error) {
set.status = 400;
return {
success: false,
message: error instanceof Error ? error.message : String(error),
};
}
},
{
params: t.Object({
deviceId: t.String(),
}),
detail: {
summary: 'List Contacts',
description: 'Get all contacts with last message.',
},
);
},
);
7 changes: 4 additions & 3 deletions src/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { logger } from '@/logger';
import { SessionManager } from '@/whatsapp';
import { openapi } from '@elysiajs/openapi';
import { file } from 'bun';
import { Elysia, t } from 'elysia';
import { cpus, freemem, hostname, uptime as osUptime, totalmem } from 'node:os';
import { version } from 'package.json';
import { stringify } from 'qs';
import icon from '../../assets/icon.ico' with { type: 'file' };
Expand All @@ -13,8 +15,6 @@ import { contacts } from './contacts';
import { logs } from './logs';
import { messages } from './messages';
import { liveSse } from './sse';
import { SessionManager } from '@/whatsapp';
import { totalmem, freemem, hostname, cpus, uptime as osUptime } from 'node:os';

const startTime = Date.now();

Expand Down Expand Up @@ -177,7 +177,8 @@ const app = new Elysia()
},
sessions: {
active: sessions.length,
total_in_db: SessionManager.getInstance().getAllSessionsFromDB().length,
total_in_db:
SessionManager.getInstance().getAllSessionsFromDB().length,
},
},
};
Expand Down
15 changes: 9 additions & 6 deletions src/server/messages/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { SessionManager } from '@/whatsapp';
import { isJidGroup, isLidUser, isPnUser } from 'baileys';
import { Elysia } from 'elysia';
import { sendRoutes } from './send';
import { manageRoutes } from './manage';
import { mediaRoutes } from './media';
import { sendRoutes } from './send';

const manager = SessionManager.getInstance();

export const getWA = (deviceId: string) => {
const whatsapp = manager.getSession(deviceId);
if (!whatsapp) throw new Error(`Device ${deviceId} not found, please connect first`);
if (!whatsapp)
throw new Error(`Device ${deviceId} not found, please connect first`);
return whatsapp;
};

Expand All @@ -24,8 +24,11 @@ export const validateJid = (jid: string) => {

export const messages = new Elysia({
prefix: '/messages',
detail: { tags: ['Messages'], summary: 'Messages', description: 'Endpoints to manage and send messages.' },
detail: {
tags: ['Messages'],
summary: 'Messages',
description: 'Endpoints to manage and send messages.',
},
})
.use(sendRoutes)
.use(manageRoutes)
.use(mediaRoutes);
.use(manageRoutes);
Loading