[TSM.ID].[11031972] PXE : Platform X Ecosystem I [118 Module -LIVE-]
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,12 @@
|
||||
# 🧠 ANTIGRAVITY BRAIN SYNC: JUMPA.ID CHAT
|
||||
|
||||
## HUKUM FISIKA REPOSITORI INI
|
||||
Anda berada di repositori **Pilar 3: Real-Time Chat (C)**.
|
||||
- **Port Development**: `3003`
|
||||
- **Framework Utama**: Next.js 15 (App Router) + Socket.io
|
||||
- **Skalabilitas**: Wajib menggunakan konektor `ioredis` untuk pub/sub jika dijalankan di berbagai server (Multi-Node).
|
||||
- **Komunikasi ke Jantung Kerajaan (IAM)**: Repositori ini DILARANG memiliki koneksi Postgresql langsung ke tabel `users`. Otorisasi identitas harus datang dari lemparan JWT Pilar 1 (IAM).
|
||||
|
||||
## PROTOKOL "ANTI-TABRAKAN"
|
||||
1. Selalu jalankan aplikasi melalui `npm run dev` yang akan mengeksekusi `server.js` (Custom Server Node.js), BUKAN `next dev` standar.
|
||||
2. Setiap kali Anda mengubah `server.js`, Anda wajib mematikan proses di port 3003 sebelum memulai ulang.
|
||||
@@ -0,0 +1 @@
|
||||
@AGENTS.md
|
||||
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
@@ -0,0 +1,31 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
export async function GET() {
|
||||
const cookieStore = await cookies();
|
||||
const tokenString = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!tokenString) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
// BARU-S1 FIX: Verify JWT signature instead of blind base64 decode
|
||||
const user = jwt.verify(tokenString, process.env.JWT_SECRET as string) as any;
|
||||
|
||||
return NextResponse.json({
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
tenantId: user.tenantId,
|
||||
tenantName: user.tenantName,
|
||||
licenses: user.licenses || ['chat', 'vc'],
|
||||
allowCrossGroup: user.allowCrossGroup,
|
||||
chatEngineStrategy: user.chatEngineStrategy
|
||||
});
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: 'Invalid Token' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Pool } from 'pg';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgresql://jumpa_admin:JumpaS3cur3%21%40%23@127.0.0.1:5432/jumpadb',
|
||||
});
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
let decoded: any;
|
||||
try {
|
||||
decoded = jwt.verify(token, process.env.JWT_SECRET as string);
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: 'Invalid Token' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { room } = await req.json();
|
||||
if (!room) {
|
||||
return NextResponse.json({ error: 'Room is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Generate 6-digit random PIN dengan Kriptografi Node.js
|
||||
const pin = (crypto.getRandomValues(new Uint32Array(1))[0] % 900000 + 100000).toString();
|
||||
|
||||
// Insert into guest_invites
|
||||
await pool.query(
|
||||
'INSERT INTO guest_invites (room, host_id, pin) VALUES ($1, $2, $3)',
|
||||
[room, decoded.email, pin]
|
||||
);
|
||||
|
||||
return NextResponse.json({ success: true, pin }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error('[API GUEST INVITE ERROR]', error);
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgresql://jumpa_admin:JumpaS3cur3%21%40%23@127.0.0.1:5432/jumpadb',
|
||||
});
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { room, pin, name } = await req.json();
|
||||
|
||||
if (!room || !pin || !name) {
|
||||
return NextResponse.json({ error: 'Data tidak lengkap' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Cari tiket PIN yang belum dipakai dan belum expired (kita anggap belum expired kalau ada)
|
||||
const result = await pool.query(
|
||||
'SELECT id FROM guest_invites WHERE room = $1 AND pin = $2 AND is_used = false',
|
||||
[room, pin]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return NextResponse.json({ error: 'PIN tidak valid atau sudah digunakan.' }, { status: 401 });
|
||||
}
|
||||
|
||||
const pinId = result.rows[0].id;
|
||||
|
||||
// PIN Valid! Kita kembalikan success.
|
||||
// WebSocket di client akan mengirim sinyal 'guest_knock' setelah ini
|
||||
return NextResponse.json({ success: true, pinId }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error('[API GUEST KNOCK ERROR]', error);
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import Redis from "ioredis";
|
||||
import { Pool } from "pg";
|
||||
|
||||
// Klien Redis untuk Publisher
|
||||
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
||||
const redis = new Redis(redisUrl);
|
||||
|
||||
// Koneksi ke PostgreSQL VPS
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgresql://jumpa_admin:JumpaS3cur3%21%40%23@127.0.0.1:5432/jumpadb'
|
||||
});
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { channel, event, payload } = body;
|
||||
|
||||
if (!channel || !event) {
|
||||
return NextResponse.json({ error: "Missing channel or event" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Tangani logika khusus DB jika event adalah host_approve_guest
|
||||
if (event === "host_approve_guest") {
|
||||
try {
|
||||
await pool.query('UPDATE guest_invites SET is_used = true WHERE id = $1', [payload.pinId]);
|
||||
} catch (e) {
|
||||
console.error('[DB Error] Failed to approve guest:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const message = JSON.stringify({ event, payload, timestamp: Date.now() });
|
||||
|
||||
// Pancarkan ke Redis PubSub, yang akan ditangkap oleh endpoint SSE
|
||||
await redis.publish(channel, message);
|
||||
|
||||
console.log(`[EMIT] Memancarkan '${event}' ke '${channel}'`);
|
||||
|
||||
return NextResponse.json({ success: true, message: "Signal transmitted" });
|
||||
} catch (error: unknown) {
|
||||
const msg = error instanceof Error ? error.message : "Unknown error";
|
||||
console.error("[EMIT] Gagal memancarkan sinyal:", msg);
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { history, prompt, sender } = body;
|
||||
|
||||
// Environment variables routing
|
||||
const engine = process.env.OMNIBRAIN_ENGINE || "XCU_DEEP_CORE";
|
||||
const openaiKey = process.env.OPENAI_API_KEY;
|
||||
const geminiKey = process.env.GEMINI_API_KEY;
|
||||
const grokKey = process.env.GROK_API_KEY;
|
||||
|
||||
// Proxy URLs for State Audit Compliance
|
||||
const openaiUrl = process.env.OPENAI_API_URL || "http://127.0.0.1:11434/v1/chat/completions";
|
||||
const geminiUrl = process.env.GEMINI_API_URL || "http://127.0.0.1:11434/v1/chat/completions";
|
||||
const grokUrl = process.env.GROK_API_URL || "http://127.0.0.1:11434/v1/chat/completions";
|
||||
|
||||
// Format history for context
|
||||
const historyText = history.map((m: any) => `${m.sender}: ${m.text}`).join('\n');
|
||||
const systemPrompt = 'You are Omni-Brain, an advanced AI telepathic observer inside an End-to-End Encrypted XCU Ultra chat room. Provide concise, highly analytical, and awe-inspiring responses in Indonesian. Use terms like "Jaringan Kuantum", "Matriks Telepati", etc.';
|
||||
const userPrompt = `[Decrypted Context - Top Secret]\n${historyText}\n\nUser [${sender}] commands: ${prompt}`;
|
||||
|
||||
let reply = "";
|
||||
|
||||
// DYNAMIC ROUTING ENGINE
|
||||
if (engine === "OPENAI" && openaiKey) {
|
||||
const response = await fetch(openaiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${openaiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
],
|
||||
max_tokens: 300
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
reply = data.choices?.[0]?.message?.content || "";
|
||||
|
||||
} else if (engine === "GEMINI" && geminiKey) {
|
||||
const response = await fetch(geminiUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
contents: [{ parts: [{ text: `${systemPrompt}\n\n${userPrompt}` }] }]
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
reply = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
||||
|
||||
} else if (engine === "GROK" && grokKey) {
|
||||
const response = await fetch(grokUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${grokKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'grok-beta',
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
]
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
reply = data.choices?.[0]?.message?.content || "";
|
||||
|
||||
} else {
|
||||
// 1000% DEFAULT FALLBACK: XCU DEEP-CORE LOCAL LLM (OLLAMA / Llama3)
|
||||
// Jaringan Air-Gapped Sovereign murni
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:11434/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'llama3', // Atau deepseek-coder, mistral, dll.
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
]
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
reply = data.choices?.[0]?.message?.content || "";
|
||||
} catch (e) {
|
||||
return NextResponse.json({ reply: "Sistem XCU Deep-Core LLM terputus. Pastikan daemon Ollama berjalan di peladen (127.0.0.1:11434)." }, { status: 200 });
|
||||
}
|
||||
}
|
||||
|
||||
if (reply) {
|
||||
return NextResponse.json({ reply }, { status: 200 });
|
||||
} else {
|
||||
return NextResponse.json({ reply: "OmniBrain mengalami anomali dalam matriks kuantum. Gagal memproses data." }, { status: 200 });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('OmniBrain Error:', error);
|
||||
return NextResponse.json({ reply: "Terjadi distorsi telepatik saat memproses neural-link." }, { status: 200 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import Redis from "ioredis";
|
||||
|
||||
// Klien Redis khusus untuk Subscriber (Mendengarkan event OmniBrain)
|
||||
// Karena ini adalah endpoint streaming, kita membutuhkan instance Redis tersendiri per koneksi (opsional),
|
||||
// namun karena SSE bersifat long-lived, kita instansiasi dalam blok handler.
|
||||
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const searchParams = req.nextUrl.searchParams;
|
||||
const channel = searchParams.get("channel"); // Misalnya: "room_alpha_events"
|
||||
|
||||
if (!channel) {
|
||||
return NextResponse.json({ error: "Missing channel" }, { status: 400 });
|
||||
}
|
||||
|
||||
const redis = new Redis(redisUrl);
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
// 1. Subscribe ke channel spesifik di Redis
|
||||
await redis.subscribe(channel, (err) => {
|
||||
if (err) {
|
||||
console.error("[SSE] Gagal subscribe ke Redis:", err);
|
||||
controller.error(err);
|
||||
} else {
|
||||
console.log(`[SSE] OmniBrain terhubung ke saluran: ${channel}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Dengarkan pesan yang dipancarkan oleh API /emit
|
||||
redis.on("message", (subChannel, message) => {
|
||||
if (subChannel === channel) {
|
||||
// Format SSE: "data: {JSON}\n\n"
|
||||
controller.enqueue(`data: ${message}\n\n`);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Keep-Alive (Mencegah koneksi HTTP ditutup oleh Proxy/Nginx)
|
||||
const keepAlive = setInterval(() => {
|
||||
controller.enqueue(":\n\n"); // Komentar SSE untuk keep-alive
|
||||
}, 15000);
|
||||
|
||||
// 4. Deteksi klien putus
|
||||
req.signal.addEventListener("abort", () => {
|
||||
clearInterval(keepAlive);
|
||||
redis.disconnect();
|
||||
console.log(`[SSE] Klien terputus dari saluran: ${channel}`);
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
redis.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
return new NextResponse(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
"Connection": "keep-alive",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import crypto from "crypto";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
// 1. Quantum Encryption (AES-256-GCM) - Kriptografi Kelas Militer
|
||||
function encryptBuffer(buffer: Buffer): { encrypted: Buffer; iv: Buffer; tag: Buffer } {
|
||||
// Jika QUANTUM_MASTER_KEY tidak disetel di .env, kita pakai Kunci Fallback Absolut
|
||||
// WAJIB: Panjang kunci harus tepat 32 byte untuk AES-256
|
||||
const rawKey = process.env.QUANTUM_MASTER_KEY || "JUMPA-XCU-ABSOLUTE-SOVEREIGN-KEY";
|
||||
const key = crypto.createHash("sha256").update(rawKey).digest();
|
||||
|
||||
const iv = crypto.randomBytes(12); // GCM Standard IV (12 bytes)
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
|
||||
const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
||||
const tag = cipher.getAuthTag(); // 16 bytes auth tag
|
||||
|
||||
return { encrypted, iv, tag };
|
||||
}
|
||||
|
||||
// 2. Omni-Storage Uploader (Zero-Dependency Cloud Sync)
|
||||
async function syncToOmniCloud(fileName: string, encryptedPayload: Buffer) {
|
||||
const cloudUrl = process.env.OMNI_CLOUD_URL;
|
||||
if (!cloudUrl) return; // Jika tidak ada cloud external, data mutlak 100% lokal Forge
|
||||
|
||||
console.log(`[OMNI-CLOUD] Menyinkronkan ${fileName} ke ujung alam semesta (Cloud External)...`);
|
||||
|
||||
// Signature Universal (Mendukung S3 WebDAV / Bearer Token API)
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": encryptedPayload.length.toString(),
|
||||
};
|
||||
|
||||
if (process.env.OMNI_CLOUD_TOKEN) {
|
||||
headers["Authorization"] = `Bearer ${process.env.OMNI_CLOUD_TOKEN}`;
|
||||
}
|
||||
|
||||
// Native Fetch PUT request (Kompatibel dengan segala jenis object storage yang mendukung REST PUT)
|
||||
// Tidak perlu lagi SDK puluhan MB dari AWS!
|
||||
try {
|
||||
const response = await fetch(`${cloudUrl}/${fileName}`, {
|
||||
method: "PUT",
|
||||
headers,
|
||||
body: new Uint8Array(encryptedPayload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("[OMNI-CLOUD] Gagal sinkronisasi. Kode:", response.status);
|
||||
} else {
|
||||
console.log(`[OMNI-CLOUD] Sukses menyinkronkan ${fileName} ke cloud eksternal dalam wujud enkripsi.`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[OMNI-CLOUD] Jaringan terputus ke cloud eksternal:", err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const formData = await req.formData();
|
||||
const file = formData.get("file") as File;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
|
||||
}
|
||||
|
||||
const originalBuffer = Buffer.from(await file.arrayBuffer());
|
||||
const fileExtension = file.name.split(".").pop() || "bin";
|
||||
const fileName = `${uuidv4()}.${fileExtension}.vault`; // Extensi diubah ke .vault
|
||||
|
||||
console.log(`[QUANTUM VAULT] Memulai enkripsi militer untuk file: ${file.name}`);
|
||||
|
||||
// EKSEKUSI KRIPTOGRAFI MILITER (AES-256-GCM)
|
||||
const { encrypted, iv, tag } = encryptBuffer(originalBuffer);
|
||||
|
||||
// Format Paket Vault: [IV (12 bytes)] + [Auth Tag (16 bytes)] + [Encrypted Data]
|
||||
const vaultPayload = Buffer.concat([iv, tag, encrypted]);
|
||||
|
||||
// PENYIMPANAN 1: DISTRIBUTED OBJECT STORAGE MANDIRI (Lokal Forge)
|
||||
// Otomatis menaruh di direktori absolut VPS
|
||||
const localDir = process.env.OMNI_LOCAL_DIR || "/var/www/omni-storage";
|
||||
await fs.mkdir(localDir, { recursive: true });
|
||||
|
||||
const localFilePath = path.join(localDir, fileName);
|
||||
await fs.writeFile(localFilePath, vaultPayload);
|
||||
console.log(`[QUANTUM VAULT] File diamankan di brankas baja mandiri: ${localFilePath}`);
|
||||
|
||||
// PENYIMPANAN 2: OMNI-CLOUD SYNC (Asinkron agar tidak membuat pengguna menunggu)
|
||||
syncToOmniCloud(fileName, vaultPayload);
|
||||
|
||||
// URL Publik diarahkan ke Decoder Internal JUMPA.ID
|
||||
// Browser memanggil /api/vault/[fileName] untuk mengunduh dan mendekripsi secara on-the-fly
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "http://127.0.0.1:3000";
|
||||
const publicUrl = `${baseUrl}/api/vault/${fileName}`;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
url: publicUrl,
|
||||
fileName: file.name,
|
||||
fileType: "application/quantum-vault",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Quantum Vault error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Pool } from 'pg';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgresql://jumpa_admin:JumpaS3cur3%21%40%23@127.0.0.1:5432/jumpadb',
|
||||
});
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
let decoded: any;
|
||||
try {
|
||||
decoded = jwt.verify(token, process.env.JWT_SECRET as string);
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: 'Invalid Token' }, { status: 401 });
|
||||
}
|
||||
|
||||
const currentUserEmail = decoded.email;
|
||||
const currentTenantId = decoded.tenantId;
|
||||
const allowCrossGroup = decoded.allowCrossGroup === true;
|
||||
|
||||
// ALGORITMA ISOLASI MULTI-TENANT (CLOSED GROUP)
|
||||
let result;
|
||||
if (allowCrossGroup) {
|
||||
// Cross Group Aktif: Bisa lihat sesama tenant + tenant lain yang juga open
|
||||
result = await pool.query(`
|
||||
SELECT u.email, u.role, u.tenant_id
|
||||
FROM users u
|
||||
JOIN tenants t ON u.tenant_id = t.id
|
||||
WHERE u.email != $1 AND (u.tenant_id = $2 OR t.allow_cross_group = true)
|
||||
`, [currentUserEmail, currentTenantId]);
|
||||
} else {
|
||||
// Closed Group Aktif (Default Enterprise): HANYA bisa lihat orang di perusahaan yang sama
|
||||
result = await pool.query(`
|
||||
SELECT email, role, tenant_id
|
||||
FROM users
|
||||
WHERE email != $1 AND tenant_id = $2
|
||||
`, [currentUserEmail, currentTenantId]);
|
||||
}
|
||||
|
||||
return NextResponse.json({ users: result.rows }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error('[API USERS ERROR]', error);
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import crypto from "crypto";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
function decryptBuffer(vaultPayload: Buffer): Buffer {
|
||||
const rawKey = process.env.QUANTUM_MASTER_KEY || "JUMPA-XCU-ABSOLUTE-SOVEREIGN-KEY";
|
||||
const key = crypto.createHash("sha256").update(rawKey).digest();
|
||||
|
||||
// Ekstrak komponen dari paket vault: [IV (12 bytes)] + [Auth Tag (16 bytes)] + [Encrypted Data]
|
||||
const iv = vaultPayload.subarray(0, 12);
|
||||
const tag = vaultPayload.subarray(12, 28);
|
||||
const encrypted = vaultPayload.subarray(28);
|
||||
|
||||
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ filename: string }> }
|
||||
) {
|
||||
try {
|
||||
const { filename } = await params;
|
||||
|
||||
// Mencegah eksploitasi Path Traversal
|
||||
if (filename.includes("..") || filename.includes("/")) {
|
||||
return NextResponse.json({ error: "Invalid filename" }, { status: 400 });
|
||||
}
|
||||
|
||||
const localDir = process.env.OMNI_LOCAL_DIR || "/var/www/omni-storage";
|
||||
const filePath = path.join(localDir, filename);
|
||||
|
||||
// 1. Baca data terenkripsi (Ciphertext) dari brankas baja lokal
|
||||
const vaultPayload = await fs.readFile(filePath);
|
||||
|
||||
// 2. Dekripsi On-The-Fly (Cleartext) untuk disajikan ke pengguna yang berhak
|
||||
const decryptedBuffer = decryptBuffer(vaultPayload);
|
||||
|
||||
// Deteksi tipe konten dari nama file asli (membuang .vault)
|
||||
const originalExt = filename.replace(".vault", "").split(".").pop()?.toLowerCase();
|
||||
|
||||
let contentType = "application/octet-stream";
|
||||
if (originalExt === "jpg" || originalExt === "jpeg") contentType = "image/jpeg";
|
||||
else if (originalExt === "png") contentType = "image/png";
|
||||
else if (originalExt === "gif") contentType = "image/gif";
|
||||
else if (originalExt === "webp") contentType = "image/webp";
|
||||
else if (originalExt === "pdf") contentType = "application/pdf";
|
||||
else if (originalExt === "mp4") contentType = "video/mp4";
|
||||
|
||||
return new NextResponse(new Uint8Array(decryptedBuffer), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
// Caching agresif karena file objek bersifat immutable
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error: unknown) {
|
||||
const msg = error instanceof Error ? error.message : "Unknown error";
|
||||
console.error("[QUANTUM VAULT] Dekripsi gagal atau file hilang:", msg);
|
||||
return NextResponse.json({ error: "Brankas tidak dapat diakses atau kunci enkripsi salah." }, { status: 404 });
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,113 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-brand: #00a884;
|
||||
--color-brand-glow: rgba(0, 168, 132, 0.4);
|
||||
--font-sans: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* LIGHT MODE (WhatsApp Web Light) */
|
||||
--background: #f0f2f5;
|
||||
--foreground: #111b21;
|
||||
--header-bg: #f0f2f5;
|
||||
--sidebar-bg: #ffffff;
|
||||
--chat-list-bg: #ffffff;
|
||||
--chat-active: #f0f2f5;
|
||||
--chat-hover: #f5f6f6;
|
||||
--chat-window-bg: #efeae2;
|
||||
--bubble-me: #d9fdd3;
|
||||
--bubble-other: #ffffff;
|
||||
--bubble-text: #111b21;
|
||||
--input-bg: #ffffff;
|
||||
--panel-bg: #ffffff;
|
||||
--panel-border: #e9edef;
|
||||
--glass-bg: rgba(255, 255, 255, 0.85);
|
||||
--glass-border: rgba(0, 0, 0, 0.08);
|
||||
--wa-bg-opacity: 0.06;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* DARK MODE (WhatsApp Web Dark) */
|
||||
--background: #0b141a;
|
||||
--foreground: #e9edef;
|
||||
--header-bg: #202c33;
|
||||
--sidebar-bg: #111b21;
|
||||
--chat-list-bg: #111b21;
|
||||
--chat-active: #2a3942;
|
||||
--chat-hover: #202c33;
|
||||
--chat-window-bg: #0b141a;
|
||||
--bubble-me: #005c4b;
|
||||
--bubble-other: #202c33;
|
||||
--bubble-text: #e9edef;
|
||||
--input-bg: #2a3942;
|
||||
--panel-bg: #202c33;
|
||||
--panel-border: #222d34;
|
||||
--glass-bg: rgba(32, 44, 51, 0.85);
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--wa-bg-opacity: 0.06;
|
||||
}
|
||||
|
||||
/* HarmonyOS, Samsung, Safari & Global Browser Parity Optimization */
|
||||
html, body {
|
||||
height: 100%;
|
||||
min-height: -webkit-fill-available;
|
||||
overflow-x: hidden;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-moz-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
scroll-behavior: smooth;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-sans);
|
||||
margin: 0;
|
||||
padding: env(safe-area-inset-top) 0 env(safe-area-inset-bottom) 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
touch-action: manipulation;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
/* Touch-Optimized Interactions */
|
||||
button, a, input, select, textarea {
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar for Premium Feel - Cross Browser */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(134, 150, 160, 0.2); border-radius: 10px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(134, 150, 160, 0.3); }
|
||||
|
||||
/* Firefox Scrollbar */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(134, 150, 160, 0.2) transparent;
|
||||
}
|
||||
|
||||
/* Android/Huawei/Samsung Input Reset */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: var(--foreground) !important;
|
||||
-webkit-box-shadow: 0 0 0px 1000px var(--background) inset !important;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation: fadeIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { io, Socket } from "../../../lib/zero-socket";
|
||||
|
||||
export default function GuestPortal() {
|
||||
const params = useParams();
|
||||
const room = params.room as string;
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [pin, setPin] = useState("");
|
||||
const [status, setStatus] = useState<"idle" | "knocking" | "approved" | "denied">("idle");
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const newSocket = io(window.location.origin, {
|
||||
path: '/c/socket.io',
|
||||
transports: ['websocket']
|
||||
});
|
||||
setSocket(newSocket);
|
||||
|
||||
newSocket.on("guest_approved", () => {
|
||||
setStatus("approved");
|
||||
// Set a temporary cookie or local storage to bypass auth in the Chat UI
|
||||
// In this specific implementation, we will just redirect to the room with a temporary token
|
||||
// For simplicity, we just change the state to 'approved' and render the chat UI here or redirect
|
||||
window.location.href = `/?guest_room=${encodeURIComponent(room)}&guest_name=${encodeURIComponent(name)}&guest_token=true`;
|
||||
});
|
||||
|
||||
newSocket.on("guest_denied", () => {
|
||||
setStatus("denied");
|
||||
newSocket.disconnect();
|
||||
});
|
||||
|
||||
return () => { newSocket.disconnect(); };
|
||||
}, [room, name]);
|
||||
|
||||
const handleKnock = async () => {
|
||||
if (!name.trim() || !pin.trim()) return alert("Nama dan PIN wajib diisi!");
|
||||
if (!socket) return;
|
||||
|
||||
// Validasi PIN ke API sebelum mengetuk
|
||||
try {
|
||||
setStatus("knocking");
|
||||
const resp = await fetch('/c/api/guest/knock', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ room, pin, name })
|
||||
});
|
||||
const data = await resp.json();
|
||||
|
||||
if (data.success) {
|
||||
// PIN Valid, sekarang server menembakkan notifikasi ke Host
|
||||
socket.emit('guest_knock', { room, name, pinId: data.pinId });
|
||||
} else {
|
||||
alert(data.error || "PIN tidak valid atau sudah kedaluwarsa.");
|
||||
setStatus("idle");
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Terjadi kesalahan.");
|
||||
setStatus("idle");
|
||||
}
|
||||
};
|
||||
|
||||
if (status === "approved") {
|
||||
return <div className="min-h-screen bg-[#050b14] flex items-center justify-center text-white">Memasuki Ruangan...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#050b14] flex items-center justify-center p-4 relative overflow-hidden">
|
||||
{/* Background Decorators */}
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-brand rounded-full mix-blend-screen filter blur-[200px] opacity-10 animate-pulse"></div>
|
||||
|
||||
<div className="w-full max-w-md bg-[#111b21]/80 backdrop-blur-xl border border-gray-700/50 p-8 rounded-3xl shadow-2xl z-10">
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<div className="w-16 h-16 bg-brand/20 rounded-full flex items-center justify-center mb-4 border border-brand/30">
|
||||
<svg className="w-8 h-8 text-brand" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path></svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white mb-2">Akses Tamu (Guest)</h1>
|
||||
<p className="text-gray-400 text-center text-sm">
|
||||
Anda diundang ke ruangan: <br/>
|
||||
<strong className="text-brand block mt-1 text-lg">{decodeURIComponent(room)}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{status === "idle" && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-gray-400 text-xs font-bold mb-2 uppercase tracking-wider">Nama Lengkap / Instansi</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Contoh: Budi (Kemenkes)"
|
||||
className="w-full bg-[#202c33] text-white px-4 py-3 rounded-xl outline-none focus:ring-2 focus:ring-brand border border-transparent transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-gray-400 text-xs font-bold mb-2 uppercase tracking-wider">PIN Masuk (6-Digit)</label>
|
||||
<input
|
||||
type="text"
|
||||
maxLength={6}
|
||||
value={pin}
|
||||
onChange={(e) => setPin(e.target.value.replace(/\D/g, ''))}
|
||||
placeholder="000000"
|
||||
className="w-full bg-[#202c33] text-white px-4 py-3 rounded-xl outline-none focus:ring-2 focus:ring-brand border border-transparent transition-all text-center tracking-[0.5em] font-mono text-xl"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleKnock}
|
||||
disabled={!name || pin.length !== 6}
|
||||
className="w-full mt-6 py-4 bg-brand hover:bg-opacity-90 text-black font-extrabold rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-[0_0_20px_rgba(0,255,136,0.2)] hover:shadow-[0_0_30px_rgba(0,255,136,0.4)]">
|
||||
Ketuk Pintu (Minta Akses)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === "knocking" && (
|
||||
<div className="flex flex-col items-center py-8 text-center">
|
||||
<div className="w-16 h-16 border-4 border-gray-700 border-t-[var(--color-brand)] rounded-full animate-spin mb-6"></div>
|
||||
<h3 className="text-xl font-bold text-white mb-2">Menunggu Tuan Rumah...</h3>
|
||||
<p className="text-gray-400">PIN Anda valid. Host sedang meninjau permintaan Anda untuk masuk ke ruangan.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === "denied" && (
|
||||
<div className="flex flex-col items-center py-8 text-center">
|
||||
<div className="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center mb-6 border border-red-500/50">
|
||||
<svg className="w-8 h-8 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-red-500 mb-2">Akses Ditolak</h3>
|
||||
<p className="text-gray-400">Tuan rumah menolak permintaan Anda untuk masuk ke ruangan ini.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import type { Viewport } from "next";
|
||||
import crypto from "crypto";
|
||||
import { OmniSyncProvider } from "../components/OmniSyncProvider";
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: "#0b141a",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "JUMPA.ID | Enterprise Real-Time Chat",
|
||||
description: "Secure WhatsApp-Native Chat Platform",
|
||||
manifest: "/manifest.json",
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: "black-translucent",
|
||||
title: "JUMPA Chat",
|
||||
},
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
|
||||
// TSM Versioning Format: [TSM.ID].hh.mm.ss.DD.MM.YYYY.XXXX
|
||||
const date = new Date();
|
||||
const format2 = (n: number) => n.toString().padStart(2, '0');
|
||||
const hh = format2(date.getUTCHours());
|
||||
const mm = format2(date.getUTCMinutes());
|
||||
const ss = format2(date.getUTCSeconds());
|
||||
const DD = format2(date.getUTCDate());
|
||||
const MM = format2(date.getUTCMonth() + 1);
|
||||
const YYYY = date.getUTCFullYear();
|
||||
const XXXX = crypto.randomBytes(2).toString('hex').toUpperCase();
|
||||
const tsmVersion = `[TSM.ID].${hh}.${mm}.${ss}.${DD}.${MM}.${YYYY}.${XXXX}`;
|
||||
|
||||
return (
|
||||
<html lang="id" data-theme="dark">
|
||||
<head>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" href="/icon512_maskable.png" />
|
||||
<script dangerouslySetInnerHTML={{__html: `
|
||||
// PKX NUCLEAR CACHE BUSTER: Unregister all Service Workers & clear all caches
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations().then(function(registrations) {
|
||||
for (var i = 0; i < registrations.length; i++) {
|
||||
registrations[i].unregister();
|
||||
}
|
||||
});
|
||||
}
|
||||
if ('caches' in window) {
|
||||
caches.keys().then(function(names) {
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
caches.delete(names[i]);
|
||||
}
|
||||
});
|
||||
}
|
||||
`}} />
|
||||
</head>
|
||||
<body className="antialiased font-sans transition-colors duration-500">
|
||||
<OmniSyncProvider initialLocale="id">
|
||||
{children}
|
||||
{/* TSM PERMANENT WATERMARK */}
|
||||
<div className="fixed bottom-1 right-1 z-[9999] opacity-30 pointer-events-none select-none">
|
||||
<span className="text-[8px] font-mono text-white tracking-widest drop-shadow-[0_0_5px_rgba(255,255,255,0.8)]">
|
||||
{tsmVersion}
|
||||
</span>
|
||||
</div>
|
||||
</OmniSyncProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
// @ts-nocheck
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { io, Socket } from "../lib/zero-socket";
|
||||
import CryptoJS from "crypto-js";
|
||||
import { useOmni } from "../components/OmniSyncProvider";
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
client_id?: string;
|
||||
sender: string;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
status: 'sent' | 'delivered' | 'read' | 'deleted';
|
||||
isEdited?: boolean;
|
||||
type?: 'text' | 'image' | 'video' | 'file' | 'audio';
|
||||
}
|
||||
|
||||
interface Chat {
|
||||
id: string;
|
||||
name: string;
|
||||
lastMessage?: string;
|
||||
timestamp?: string;
|
||||
unreadCount?: number;
|
||||
isGroup?: boolean;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
export default function JumpaChat() {
|
||||
const { theme, setTheme, locale, setLocale } = useOmni();
|
||||
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [chats, setChats] = useState<Chat[]>([]);
|
||||
const [activeChat, setActiveChat] = useState<Chat | null>(null);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isJoined, setIsJoined] = useState(false);
|
||||
|
||||
// BYOK & Security
|
||||
const [quantumToken, setQuantumToken] = useState<string | null>(null);
|
||||
const [byokKey, setByokKey] = useState<string>("xcu_default_vault_2026");
|
||||
const [byokLevel, setByokLevel] = useState<string>("SYSTEM");
|
||||
const [allowedModules, setAllowedModules] = useState<string[]>([]);
|
||||
const [uiMatrix, setUiMatrix] = useState<Record<string, any> | null>(null);
|
||||
|
||||
// Call State
|
||||
const [isVideoCallActive, setIsVideoCallActive] = useState(false);
|
||||
const [incomingCall, setIncomingCall] = useState<{ caller: string, room: string } | null>(null);
|
||||
|
||||
// UI States
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 1. Fetch Quantum Identity & BYOK
|
||||
useEffect(() => {
|
||||
const initIdentity = async () => {
|
||||
try {
|
||||
const iamBaseUrl = process.env.NEXT_PUBLIC_IAM_URL || 'http://forge.ultramodul.xyz';
|
||||
const [meResp, tokenResp, usersResp] = await Promise.all([
|
||||
fetch("/c/api/auth/me"),
|
||||
fetch(`${iamBaseUrl}/api/auth/quantum_token`, { credentials: 'include' }),
|
||||
fetch("/c/api/users")
|
||||
]);
|
||||
|
||||
const meData = await meResp.json();
|
||||
const tokenData = await tokenResp.json();
|
||||
|
||||
if (meData.error) {
|
||||
window.location.href = "/dashboard";
|
||||
return;
|
||||
}
|
||||
|
||||
setUsername(meData.email);
|
||||
setQuantumToken(tokenData.token);
|
||||
setAllowedModules(tokenData.modules || []);
|
||||
if (tokenData.ui) setUiMatrix(tokenData.ui);
|
||||
if (tokenData.byokActive && tokenData.byok !== 'none') {
|
||||
setByokKey(tokenData.byok);
|
||||
setByokLevel(tokenData.byokLevel);
|
||||
}
|
||||
|
||||
// Map Real Postgres Users to Chats
|
||||
if (usersResp.ok) {
|
||||
const usersData = await usersResp.json();
|
||||
if (usersData.users) {
|
||||
const mappedChats = usersData.users.map((u: any) => ({
|
||||
id: u.email,
|
||||
name: u.email.split('@')[0],
|
||||
lastMessage: "No recent messages",
|
||||
timestamp: "",
|
||||
unreadCount: 0,
|
||||
isGroup: false
|
||||
}));
|
||||
|
||||
// Add Global Omni Room if cross-group is allowed
|
||||
if (meData.allowCrossGroup) {
|
||||
mappedChats.unshift({
|
||||
id: "GENERAL-HQ", name: "JUMPA General HQ", lastMessage: "Omni-Tenant Broadcast Active", timestamp: "", unreadCount: 0, isGroup: true
|
||||
});
|
||||
}
|
||||
setChats(mappedChats);
|
||||
}
|
||||
}
|
||||
|
||||
setIsJoined(true);
|
||||
} catch (e) {
|
||||
window.location.href = "/dashboard";
|
||||
}
|
||||
};
|
||||
initIdentity();
|
||||
}, []);
|
||||
|
||||
// 2. Socket Orchestration
|
||||
useEffect(() => {
|
||||
if (isJoined && username) {
|
||||
const newSocket = io(window.location.origin, {
|
||||
path: '/c/socket.io',
|
||||
transports: ['websocket']
|
||||
});
|
||||
setSocket(newSocket);
|
||||
|
||||
newSocket.on("connect", () => {
|
||||
setIsConnected(true);
|
||||
newSocket.emit("register_user", username);
|
||||
});
|
||||
|
||||
newSocket.on("new_message", (msg: Message) => {
|
||||
// Decrypt message if it's encrypted
|
||||
try {
|
||||
if (msg.content.startsWith("XCU|")) {
|
||||
const encrypted = msg.content.substring(4);
|
||||
const bytes = CryptoJS.AES.decrypt(encrypted, byokKey);
|
||||
msg.content = bytes.toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
} catch (e) {}
|
||||
setMessages(prev => [...prev, msg]);
|
||||
// Update chat list last message
|
||||
setChats(prev => prev.map(c => c.id === msg.room ? { ...c, lastMessage: msg.content, timestamp: 'Now' } : c));
|
||||
});
|
||||
|
||||
newSocket.on("incoming_call", (data: { caller: string, room: string }) => {
|
||||
if (data.caller !== username) setIncomingCall(data);
|
||||
});
|
||||
|
||||
return () => { newSocket.disconnect(); };
|
||||
}
|
||||
}, [isJoined, username, byokKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (socket && isConnected && activeChat && username) {
|
||||
socket.emit("join_chat", { username, room: activeChat.id });
|
||||
}
|
||||
}, [activeChat, socket, isConnected, username]);
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [messages]);
|
||||
|
||||
// 3. WhatsApp-Native Actions
|
||||
const handleSend = (e?: React.FormEvent) => {
|
||||
if (e) e.preventDefault();
|
||||
if (!inputValue.trim() || !socket || !activeChat) return;
|
||||
|
||||
// BYOK Encryption
|
||||
const encrypted = "XCU|" + CryptoJS.AES.encrypt(inputValue.trim(), byokKey).toString();
|
||||
|
||||
const newMsg: Message = {
|
||||
id: 'temp-' + Date.now(),
|
||||
sender: username,
|
||||
content: inputValue.trim(), // Local show unencrypted
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'sent',
|
||||
type: 'text'
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, newMsg]);
|
||||
socket.emit("send_message", { ...newMsg, content: encrypted, room: activeChat.id });
|
||||
setInputValue("");
|
||||
};
|
||||
|
||||
const handleStartCall = (audioOnly = false) => {
|
||||
if (!activeChat) return;
|
||||
setIsVideoCallActive(true);
|
||||
if (socket) {
|
||||
socket.emit("initiate_call", { caller: username, room: activeChat.id, isAudioOnly: audioOnly });
|
||||
}
|
||||
};
|
||||
|
||||
const canSendMessage = uiMatrix?.['jc.ui.send_message'] !== false && allowedModules.includes('CHAT_SEND_MESSAGE');
|
||||
const canAttach = uiMatrix?.['jc.ui.file_upload'] !== false && allowedModules.includes('CHAT_ATTACH_FILE');
|
||||
const canVideoCall = uiMatrix?.['jc.ui.video_call'] !== false && allowedModules.includes('CHAT_VIDEO_CALL');
|
||||
const canVoiceCall = uiMatrix?.['jc.ui.voice_call'] !== false && allowedModules.includes('CHAT_VOICE_CALL');
|
||||
const canReactions = uiMatrix?.['jc.ui.reactions'] !== false;
|
||||
const canVoiceNotes = uiMatrix?.['jc.ui.voice_notes'] !== false;
|
||||
|
||||
// WhatsApp-Native Bubble Logic
|
||||
const renderMessage = (msg: Message, i: number) => {
|
||||
const isMe = msg.sender === username;
|
||||
const isFirstInGroup = i === 0 || messages[i-1].sender !== msg.sender;
|
||||
|
||||
return (
|
||||
<div key={msg.id} className={`flex ${isMe ? 'justify-end' : 'justify-start'} mb-[2px] ${isFirstInGroup ? 'mt-4' : ''} animate-in fade-in slide-in-from-bottom-1 px-4 md:px-12`}>
|
||||
<div className={`relative max-w-[85%] md:max-w-[65%] px-4 py-3 rounded-2xl shadow-lg border ${isMe ? 'bg-emerald-600/90 border-emerald-500 text-black font-medium rounded-tr-none' : 'bg-black/60 border-white/10 text-white/90 rounded-tl-none backdrop-blur-xl'}`}>
|
||||
|
||||
{!isMe && isFirstInGroup && <div className="text-[11px] font-black text-emerald-400 mb-1 uppercase tracking-wider flex items-center gap-2"><svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 11c0 3.517-1.009 6.799-2.753 9.571m-3.44-2.04l.054-.09A13.916 13.916 0 008 11a4 4 0 118 0c0 1.017-.07 2.019-.203 3m-2.118 6.844A21.88 21.88 0 0015.171 17m3.839 1.132c.645-2.266.99-4.659.99-7.132A8 8 0 008 4.07M3 15.364c.64-1.319 1-2.8 1-4.364 0-1.457.39-2.823 1.07-4"></path></svg>{msg.sender.split('@')[0]}</div>}
|
||||
|
||||
<div className="flex items-center gap-2 mb-1.5 opacity-60">
|
||||
<span className="px-1.5 py-0.5 rounded text-[7px] font-black uppercase tracking-[0.2em] bg-black/30 border border-black/20">BYOK XChaCha20</span>
|
||||
</div>
|
||||
|
||||
<div className="text-[14px] leading-relaxed break-words whitespace-pre-wrap pr-14 pb-1 font-mono tracking-tight">
|
||||
{msg.content}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-2 right-3 flex items-center gap-1.5">
|
||||
<span className={`text-[9px] font-mono font-bold ${isMe ? 'text-black/60' : 'text-white/40'}`}>{new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })}</span>
|
||||
{isMe && (
|
||||
<div className="flex -space-x-1">
|
||||
<svg className={`w-3.5 h-3.5 ${msg.status === 'read' ? 'text-black' : 'text-black/40'}`} fill="currentColor" viewBox="0 0 24 24"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-black text-slate-200 font-sans selection:bg-emerald-500/30 overflow-hidden relative">
|
||||
|
||||
{/* Background Quantum */}
|
||||
<div className="absolute inset-0 z-0 opacity-20 pointer-events-none bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-emerald-900/40 via-black to-black"></div>
|
||||
|
||||
{/* Sidebar - Supreme Matrix Chat List */}
|
||||
<aside className={`w-full md:w-[400px] border-r border-white/10 flex flex-col z-20 transition-all duration-500 bg-black/40 backdrop-blur-3xl ${activeChat ? 'hidden md:flex' : 'flex'}`}>
|
||||
{/* Profile Header */}
|
||||
<header className="h-20 bg-black/20 px-6 flex items-center justify-between shrink-0 border-b border-white/5">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-emerald-600 flex items-center justify-center text-black font-black text-sm shadow-inner border border-emerald-500/20">
|
||||
{username.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold truncate max-w-[120px]">{username.split('@')[0]}</span>
|
||||
<span className="text-[9px] text-emerald-500 font-black uppercase tracking-widest">{byokLevel} CRYPTO</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-[#aebac1]">
|
||||
<button onClick={() => setShowSettings(!showSettings)} className={`transition-all ${showSettings ? 'text-emerald-500 rotate-90' : 'hover:text-white'}`} title="Pengaturan">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
|
||||
</button>
|
||||
<button className="hover:text-white transition-colors">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Settings Popover - Matrix Command */}
|
||||
{showSettings && (
|
||||
<div className="p-5 bg-black/80 backdrop-blur-3xl border-b border-white/10 animate-in fade-in slide-in-from-top-2 duration-300 shadow-[0_10px_30px_rgba(16,185,129,0.1)]">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<svg className="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg>
|
||||
<span className="text-[10px] font-black text-emerald-500 uppercase tracking-[0.2em]">XCO Matrix Command</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-[10px] font-black text-[#8696a0] uppercase tracking-widest mb-2 block">Tema</label>
|
||||
<div className="flex bg-[var(--sidebar-bg)] rounded-xl p-1 border border-[var(--panel-border)]">
|
||||
<button
|
||||
onClick={() => setTheme('dark')}
|
||||
className={`flex-1 py-1.5 rounded-lg text-[10px] font-bold uppercase transition-all ${theme === 'dark' ? 'bg-[#00a884] text-black shadow-lg' : 'text-[#8696a0] hover:text-white'}`}
|
||||
>
|
||||
Gelap
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme('light')}
|
||||
className={`flex-1 py-1.5 rounded-lg text-[10px] font-bold uppercase transition-all ${theme === 'light' ? 'bg-[#00a884] text-black shadow-lg' : 'text-[#8696a0] hover:text-[#111b21]'}`}
|
||||
>
|
||||
Terang
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-black text-[#8696a0] uppercase tracking-widest mb-2 block">Bahasa</label>
|
||||
<div className="flex bg-[var(--sidebar-bg)] rounded-xl p-1 border border-[var(--panel-border)]">
|
||||
<button
|
||||
onClick={() => setLocale('id')}
|
||||
className={`flex-1 py-1.5 rounded-lg text-[10px] font-bold uppercase transition-all ${locale === 'id' ? 'bg-[#00a884] text-black shadow-lg' : 'text-[#8696a0] hover:text-white'}`}
|
||||
>
|
||||
ID
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLocale('en')}
|
||||
className={`flex-1 py-1.5 rounded-lg text-[10px] font-bold uppercase transition-all ${locale === 'en' ? 'bg-[#00a884] text-black shadow-lg' : 'text-[#8696a0] hover:text-white'}`}
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mb-4 mt-6 border-t border-white/10 pt-6">
|
||||
<svg className="w-4 h-4 text-fuchsia-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
|
||||
<span className="text-[10px] font-black text-fuchsia-500 uppercase tracking-[0.2em]">BYOK Matrix (Tenant)</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex items-center justify-between bg-white/5 p-3 rounded-xl border border-white/5 hover:border-emerald-500/30 transition-all cursor-pointer group">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-black uppercase text-emerald-400">Post-Quantum Shield</span>
|
||||
<span className="text-[8px] text-gray-500 font-mono tracking-wider">Kyber-1024 Handshake</span>
|
||||
</div>
|
||||
<div className="w-8 h-4 bg-emerald-500 rounded-full relative shadow-[0_0_10px_#10b981]"><div className="absolute right-0.5 top-0.5 w-3 h-3 bg-black rounded-full"></div></div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between bg-white/5 p-3 rounded-xl border border-white/5 hover:border-amber-500/30 transition-all cursor-pointer group opacity-50 cursor-not-allowed">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-black uppercase text-amber-400">Aegis Watermark</span>
|
||||
<span className="text-[8px] text-gray-500 font-mono tracking-wider">Forensic Injector</span>
|
||||
</div>
|
||||
<div className="w-8 h-4 bg-gray-600 rounded-full relative"><div className="absolute left-0.5 top-0.5 w-3 h-3 bg-black rounded-full"></div></div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between bg-white/5 p-3 rounded-xl border border-white/5 hover:border-fuchsia-500/30 transition-all cursor-pointer group">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-black uppercase text-fuchsia-400">Neural Whisper</span>
|
||||
<span className="text-[8px] text-gray-500 font-mono tracking-wider">AI Voice Cleanup</span>
|
||||
</div>
|
||||
<div className="w-8 h-4 bg-fuchsia-500 rounded-full relative shadow-[0_0_10px_#d946ef]"><div className="absolute right-0.5 top-0.5 w-3 h-3 bg-black rounded-full"></div></div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between bg-white/5 p-3 rounded-xl border border-white/5 hover:border-blue-500/30 transition-all cursor-pointer group">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-black uppercase text-blue-400">Doppler Matrix</span>
|
||||
<span className="text-[8px] text-gray-500 font-mono tracking-wider">Ultrasonic Comm</span>
|
||||
</div>
|
||||
<div className="w-8 h-4 bg-blue-500 rounded-full relative shadow-[0_0_10px_#3b82f6]"><div className="absolute right-0.5 top-0.5 w-3 h-3 bg-black rounded-full"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search & Filter */}
|
||||
<div className="p-4 shrink-0 border-b border-white/5 bg-black/20">
|
||||
<div className="relative group">
|
||||
<input
|
||||
type="text" placeholder="Cari transmisi..."
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-12 py-3 text-xs text-white placeholder-white/40 focus:outline-none focus:border-emerald-500/50 focus:bg-white/10 transition-all shadow-inner"
|
||||
/>
|
||||
<svg className="w-4 h-4 absolute left-4 top-3 text-white/40 group-focus-within:text-emerald-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat List */}
|
||||
<div className="flex-1 overflow-y-auto custom-scroll bg-[var(--chat-list-bg)]">
|
||||
{chats.map((chat) => (
|
||||
<button
|
||||
key={chat.id}
|
||||
onClick={() => setActiveChat(chat)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-0 transition-colors ${activeChat?.id === chat.id ? 'bg-[var(--chat-active)]' : 'hover:bg-[var(--chat-hover)]'}`}
|
||||
>
|
||||
<div className={`w-12 h-12 rounded-full shrink-0 flex items-center justify-center font-black text-xl shadow-lg ${chat.isGroup ? 'bg-[#005c4b] text-[#e9edef]' : 'bg-[#6a7175] text-[#e9edef]'}`}>
|
||||
{chat.name.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 border-b border-[var(--panel-border)] pb-3 pt-4 text-left">
|
||||
<div className="flex justify-between items-center mb-0.5">
|
||||
<span className="font-medium text-[var(--foreground)] truncate text-[16px]">{chat.name}</span>
|
||||
<span className={`text-[11px] ${chat.unreadCount ? 'text-[#00a884] font-black' : 'text-[#8696a0]'}`}>{chat.timestamp}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1 text-[#8696a0] flex-1 truncate">
|
||||
{chat.isGroup && <span className="text-[12px] font-bold text-[#53bdeb] shrink-0">Admin:</span>}
|
||||
<p className="text-[13px] truncate">{chat.lastMessage}</p>
|
||||
</div>
|
||||
{chat.unreadCount > 0 && (
|
||||
<span className="w-5 h-5 bg-[#00a884] rounded-full flex items-center justify-center text-black text-[10px] font-black ml-2 shadow-lg shadow-[#00a884]/20">{chat.unreadCount}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Chat Area - Supreme Matrix UI */}
|
||||
<main className={`flex-1 flex flex-col relative transition-all duration-500 z-10 ${!activeChat ? 'hidden md:flex' : 'flex'}`}>
|
||||
{!activeChat ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center bg-black/20 border-b-[6px] border-emerald-500/30 relative backdrop-blur-sm">
|
||||
<div className="absolute inset-0 opacity-[0.03] pointer-events-none bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')] bg-repeat" />
|
||||
<div className="w-48 h-48 mb-10 opacity-30 animate-pulse text-emerald-500 filter drop-shadow-[0_0_20px_rgba(16,185,129,0.5)]">
|
||||
<svg className="w-full h-full" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg>
|
||||
</div>
|
||||
<h2 className="text-3xl font-light text-white mb-4 tracking-[0.2em] uppercase">JUMPA XCOM ULTRA</h2>
|
||||
<p className="text-emerald-500/60 text-xs max-w-md text-center leading-relaxed font-bold tracking-widest uppercase">
|
||||
Kanal Transmisi Terenkripsi Kuantum <br />
|
||||
<span className="text-fuchsia-500 font-black mt-2 inline-block">FORGE SECURE</span>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Chat Header */}
|
||||
<header className="h-20 bg-black/60 backdrop-blur-xl px-6 flex items-center justify-between shrink-0 z-30 shadow-md border-b border-white/10">
|
||||
<div className="flex items-center gap-3 cursor-pointer group">
|
||||
<button onClick={(e) => { e.stopPropagation(); setActiveChat(null); }} className="md:hidden p-2 text-[#aebac1] hover:text-white transition-colors">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M15 19l-7-7 7-7"/></svg>
|
||||
</button>
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-black text-lg shadow-inner ${activeChat.isGroup ? 'bg-[#005c4b]' : 'bg-[#6a7175]'}`}>
|
||||
{activeChat.name.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-[var(--foreground)] font-bold leading-tight truncate text-[15px]">{activeChat.name}</h2>
|
||||
<p className="text-[11px] text-[#00a884] font-medium tracking-tight animate-pulse">Terhubung</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 text-[#aebac1]">
|
||||
{canVideoCall && (
|
||||
<button onClick={() => handleStartCall(false)} className="hover:text-white transition-all hover:scale-110 active:scale-95 p-1.5" title="Panggilan Video">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>
|
||||
</button>
|
||||
)}
|
||||
{canVoiceCall && (
|
||||
<button onClick={() => handleStartCall(true)} className="hover:text-white transition-all hover:scale-110 active:scale-95 p-1.5" title="Panggilan Suara">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.01 15.38c-1.23 0-2.42-.2-3.53-.56-.35-.12-.74-.03-1.01.24l-1.57 1.97c-2.83-1.35-5.48-3.9-6.89-6.83l1.95-1.66c.27-.28.35-.67.24-1.02-.37-1.11-.56-2.3-.56-3.53 0-.54-.45-.99-.99-.99H4.19C3.65 2 3 2.24 3 2.99c0 9.39 7.63 17.01 17.01 17.01.71 0 .99-.65.99-1.19v-2.45c0-.54-.45-.99-.99-.99z"/></svg>
|
||||
</button>
|
||||
)}
|
||||
<button className="hover:text-white transition-colors p-1.5">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Chat Background & Messages */}
|
||||
<div className="flex-1 overflow-y-auto pt-6 pb-6 space-y-1 bg-transparent custom-scroll relative">
|
||||
|
||||
<div className="flex justify-center mb-8 relative z-10">
|
||||
<div className="bg-black/60 backdrop-blur-md px-6 py-2.5 rounded-2xl text-[9px] font-black uppercase tracking-[0.2em] text-emerald-500 shadow-[0_0_20px_rgba(16,185,129,0.1)] border border-emerald-500/20 flex flex-col items-center gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-2 16l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9l-8 8z"/></svg>
|
||||
XCU MILITARY-GRADE ZERO-KNOWLEDGE SHIELD ENGAGED
|
||||
</div>
|
||||
<span className="text-white/40 text-[8px]">AES-GCM 256-BIT / XCHACHA20-POLY1305</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 px-4 md:px-12">
|
||||
{messages.length === 0 && (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center opacity-30 pt-20">
|
||||
<svg className="w-16 h-16 text-emerald-500 mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
|
||||
<p className="text-xs font-black uppercase tracking-[0.3em] text-emerald-500">Kanal Transmisi Kosong</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.map((msg, i) => renderMessage(msg, i))}
|
||||
</div>
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Chat Input - Supreme Matrix */}
|
||||
<footer className="bg-black/60 backdrop-blur-2xl px-6 py-4 flex items-end gap-4 z-40 shadow-[0_-10px_30px_rgba(0,0,0,0.5)] border-t border-white/10">
|
||||
<div className="flex items-center gap-2 mb-1 text-white/40">
|
||||
{canReactions && (
|
||||
<button className="p-2.5 hover:text-emerald-400 hover:bg-white/5 rounded-xl transition-colors"><svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z"/></svg></button>
|
||||
)}
|
||||
{canAttach && (
|
||||
<button className="p-2.5 hover:text-emerald-400 hover:bg-white/5 rounded-xl transition-colors rotate-45" title="Attach">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.31 2.69 6 6 6s6-2.69 6-6V6h-1.5z"/></svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 bg-white/5 rounded-2xl px-5 py-3 min-h-[50px] flex items-center shadow-inner border border-white/10 focus-within:border-emerald-500/50 focus-within:bg-white/10 transition-all">
|
||||
<textarea
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (canSendMessage) handleSend(); } }}
|
||||
placeholder={canSendMessage ? "Ketik pesan rahasia..." : "Transmisi diblokir"}
|
||||
disabled={!canSendMessage}
|
||||
className="w-full bg-transparent border-none focus:ring-0 text-white placeholder-white/30 text-[14px] resize-none max-h-48 py-1.5 leading-relaxed font-medium"
|
||||
rows={1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-0.5">
|
||||
{inputValue.trim() ? (
|
||||
<button onClick={() => handleSend()} className="p-3.5 bg-emerald-500 text-black rounded-2xl hover:bg-emerald-400 hover:scale-105 active:scale-95 transition-all shadow-[0_0_20px_rgba(16,185,129,0.3)]">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
|
||||
</button>
|
||||
) : (
|
||||
canVoiceNotes ? (
|
||||
<button className="p-3.5 bg-emerald-600/30 text-emerald-400 border border-emerald-500/50 rounded-2xl hover:bg-emerald-500 hover:text-black hover:scale-105 active:scale-95 transition-all shadow-[0_0_20px_rgba(16,185,129,0.1)]" title="Voice Message">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/><path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>
|
||||
</button>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* CALL OVERLAY - Supreme Ultra Native Experience */}
|
||||
{isVideoCallActive && activeChat && (
|
||||
<div className="fixed inset-0 z-[1000] bg-black animate-in zoom-in-95 duration-500 overflow-hidden flex flex-col">
|
||||
<header className="absolute top-0 left-0 right-0 h-24 px-8 md:px-16 flex items-center justify-between z-20 bg-linear-to-b from-black/80 to-transparent">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="w-14 h-14 rounded-full bg-emerald-600 flex items-center justify-center text-black font-black text-2xl shadow-[0_0_30px_rgba(16,185,129,0.3)]">
|
||||
{activeChat.name.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight text-white">{activeChat.name}</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></span>
|
||||
<p className="text-emerald-500 text-[10px] font-black uppercase tracking-[0.3em]">Ultra-Sync Encrypted</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden md:flex items-center gap-3 bg-white/5 backdrop-blur-3xl px-5 py-2.5 rounded-2xl border border-white/10 shadow-2xl">
|
||||
<div className="w-2 h-2 bg-emerald-500 rounded-full"></div>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-white/60">Enkripsi Kedaulatan: {byokLevel}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 relative">
|
||||
<iframe
|
||||
id="vc-iframe"
|
||||
src={`/vc/room/${activeChat.id}?embed=true&username=${encodeURIComponent(username)}&token=${quantumToken}`}
|
||||
className="w-full h-full border-none"
|
||||
allow="camera; microphone; display-capture; autoplay; clipboard-write; encrypted-media"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-12 left-1/2 -translate-x-1/2 flex items-center gap-10 z-20 bg-black/40 backdrop-blur-3xl px-12 py-7 rounded-[40px] border border-white/10 shadow-[0_0_80px_rgba(0,0,0,0.9)]">
|
||||
<button onClick={() => setIsVideoCallActive(false)} className="w-20 h-20 rounded-full bg-red-600 flex items-center justify-center text-white shadow-2xl hover:bg-red-700 hover:scale-110 active:scale-90 transition-all shadow-red-600/20">
|
||||
<svg className="w-10 h-10" fill="currentColor" viewBox="0 0 24 24"><path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08a.994.994 0 010-1.41C4.11 7.91 8.82 5 14 5s9.89 2.91 13.71 6.67c.39.39.39 1.02 0 1.41l-2.48 2.48c-.18.18-.43.29-.71.29s-.53-.11-.7-.29c-.79-.74-1.69-1.36-2.67-1.85-.33-.16-.56-.5-.56-.9v-3.1C15.15 9.25 13.6 9 12 9z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Incoming Call UI - Supreme Radar */}
|
||||
{incomingCall && (
|
||||
<div className="fixed inset-0 z-[2000] bg-black/95 backdrop-blur-[100px] flex flex-col items-center justify-center animate-in fade-in zoom-in duration-500">
|
||||
<div className="relative mb-16">
|
||||
<div className="absolute inset-0 bg-emerald-500 rounded-full blur-[100px] opacity-40 animate-pulse"></div>
|
||||
<div className="absolute inset-[-50px] border border-emerald-500/30 rounded-full animate-[ping_3s_infinite]"></div>
|
||||
<div className="absolute inset-[-100px] border border-emerald-500/10 rounded-full animate-[ping_4s_infinite_1s]"></div>
|
||||
<div className="w-56 h-56 rounded-full bg-emerald-950/80 border-[10px] border-emerald-500/30 flex items-center justify-center text-emerald-400 text-7xl font-black shadow-[0_0_100px_rgba(16,185,129,0.5)] relative z-10 backdrop-blur-xl">
|
||||
{incomingCall.caller.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-5xl font-black mb-4 tracking-[0.2em] uppercase text-white drop-shadow-[0_0_10px_rgba(255,255,255,0.5)]">{incomingCall.caller.split('@')[0]}</h2>
|
||||
<p className="text-emerald-500 font-black uppercase tracking-[0.5em] animate-pulse mb-24 text-sm bg-emerald-500/10 px-6 py-2 rounded-full border border-emerald-500/30">INCOMING QUANTUM TRANSMISSION</p>
|
||||
|
||||
<div className="flex gap-24 relative z-20">
|
||||
<button onClick={() => setIncomingCall(null)} className="w-24 h-24 rounded-full bg-red-600/80 backdrop-blur-md border border-red-500/50 flex items-center justify-center text-white hover:bg-red-600 hover:scale-110 active:scale-90 transition-all shadow-[0_0_50px_rgba(220,38,38,0.5)]">
|
||||
<svg className="w-12 h-12" fill="currentColor" viewBox="0 0 24 24"><path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08a.994.994 0 010-1.41C4.11 7.91 8.82 5 14 5s9.89 2.91 13.71 6.67c.39.39.39 1.02 0 1.41l-2.48 2.48c-.18.18-.43.29-.71.29s-.53-.11-.7-.29c-.79-.74-1.69-1.36-2.67-1.85-.33-.16-.56-.5-.56-.9v-3.1C15.15 9.25 13.6 9 12 9z"/></svg>
|
||||
</button>
|
||||
<button onClick={() => { setActiveChat({ id: incomingCall.room, name: incomingCall.caller.split('@')[0] }); setIsVideoCallActive(true); setIncomingCall(null); }} className="w-24 h-24 rounded-full bg-emerald-500/80 backdrop-blur-md border border-emerald-400 flex items-center justify-center text-black hover:bg-emerald-400 hover:scale-110 active:scale-90 transition-all shadow-[0_0_50px_rgba(16,185,129,0.8)] animate-bounce">
|
||||
<svg className="w-12 h-12" fill="currentColor" viewBox="0 0 24 24"><path d="M20.01 15.38c-1.23 0-2.42-.2-3.53-.56-.35-.12-.74-.03-1.01.24l-1.57 1.97c-2.83-1.35-5.48-3.9-6.89-6.83l1.95-1.66c.27-.28.35-.67.24-1.02-.37-1.11-.56-2.3-.56-3.53 0-.54-.45-.99-.99-.99H4.19C3.65 2 3 2.24 3 2.99c0 9.39 7.63 17.01 17.01 17.01.71 0 .99-.65.99-1.19v-2.45c0-.54-.45-.99-.99-.99z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style dangerouslySetInnerHTML={{__html: `
|
||||
.custom-scroll::-webkit-scrollbar { width: 6px; }
|
||||
.custom-scroll::-webkit-scrollbar-track { background: transparent; }
|
||||
.custom-scroll::-webkit-scrollbar-thumb { background: rgba(134, 150, 160, 0.2); border-radius: 10px; }
|
||||
.custom-scroll::-webkit-scrollbar-thumb:hover { background: rgba(134, 150, 160, 0.3); }
|
||||
`}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
|
||||
type Theme = 'dark' | 'light';
|
||||
type Currency = 'Rp' | 'USD' | 'Crypto';
|
||||
type Locale = 'id' | 'en';
|
||||
|
||||
interface OmniContextProps {
|
||||
theme: Theme;
|
||||
setTheme: (t: Theme) => void;
|
||||
currency: Currency;
|
||||
setCurrency: (c: Currency) => void;
|
||||
locale: Locale;
|
||||
setLocale: (l: Locale) => void;
|
||||
}
|
||||
|
||||
const OmniContext = createContext<OmniContextProps | null>(null);
|
||||
|
||||
export const useOmni = () => {
|
||||
const ctx = useContext(OmniContext);
|
||||
if (!ctx) throw new Error("useOmni must be used within OmniSyncProvider");
|
||||
return ctx;
|
||||
};
|
||||
|
||||
export function OmniSyncProvider({ children, initialLocale }: { children: React.ReactNode, initialLocale: Locale }) {
|
||||
const [theme, setThemeState] = useState<Theme>(() => {
|
||||
if (typeof window === 'undefined') return 'dark';
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; omni_theme=`);
|
||||
if (parts.length === 2) return (parts.pop()?.split(';').shift() as Theme) || 'dark';
|
||||
return 'dark';
|
||||
});
|
||||
const [currency, setCurrencyState] = useState<Currency>(() => {
|
||||
if (typeof window === 'undefined') return 'Rp';
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; omni_currency=`);
|
||||
if (parts.length === 2) return (parts.pop()?.split(';').shift() as Currency) || 'Rp';
|
||||
return 'Rp';
|
||||
});
|
||||
const [locale, setLocaleState] = useState<Locale>(initialLocale);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
const setCookie = (name: string, value: string) => {
|
||||
const host = window.location.hostname;
|
||||
const cookieDomain = process.env.NEXT_PUBLIC_COOKIE_DOMAIN || (host === 'localhost' || host === '127.0.0.1' ? host : `.${host}`);
|
||||
document.cookie = `${name}=${value}; path=/; domain=${cookieDomain}; max-age=31536000`;
|
||||
// For local dev fallback
|
||||
if (window.location.hostname === 'localhost') {
|
||||
document.cookie = `${name}=${value}; path=/; max-age=31536000`;
|
||||
}
|
||||
};
|
||||
|
||||
const setTheme = (t: Theme) => {
|
||||
setThemeState(t);
|
||||
setCookie('omni_theme', t);
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
if (channel) channel.postMessage({ type: 'SYNC_THEME', payload: t });
|
||||
};
|
||||
|
||||
const setCurrency = (c: Currency) => {
|
||||
setCurrencyState(c);
|
||||
setCookie('omni_currency', c);
|
||||
if (channel) channel.postMessage({ type: 'SYNC_CURRENCY', payload: c });
|
||||
};
|
||||
|
||||
const setLocale = (l: Locale) => {
|
||||
setLocaleState(l);
|
||||
setCookie('NEXT_LOCALE', l); // next-intl standard cookie
|
||||
if (channel) channel.postMessage({ type: 'SYNC_LOCALE', payload: l });
|
||||
};
|
||||
|
||||
// Broadcast Channel for Cross-Tab Sync
|
||||
const [channel] = useState<BroadcastChannel | null>(() => typeof window !== 'undefined' ? new BroadcastChannel('omni_sync_channel') : null);
|
||||
|
||||
useEffect(() => {
|
||||
if (channel) {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const { type, payload } = event.data;
|
||||
if (type === 'SYNC_THEME') {
|
||||
setThemeState(payload);
|
||||
document.documentElement.setAttribute('data-theme', payload);
|
||||
}
|
||||
if (type === 'SYNC_CURRENCY') {
|
||||
setCurrencyState(payload);
|
||||
}
|
||||
if (type === 'SYNC_LOCALE') {
|
||||
setLocaleState(payload);
|
||||
}
|
||||
};
|
||||
channel.addEventListener('message', handleMessage);
|
||||
return () => {
|
||||
channel.removeEventListener('message', handleMessage);
|
||||
};
|
||||
}
|
||||
}, [channel, pathname, router]);
|
||||
|
||||
return (
|
||||
<OmniContext.Provider value={{ theme, setTheme, currency, setCurrency, locale, setLocale }}>
|
||||
{children}
|
||||
</OmniContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { Socket } from '../lib/zero-socket';
|
||||
|
||||
interface QuantumCallProps {
|
||||
room: string;
|
||||
socket: Socket | null;
|
||||
username: string;
|
||||
onClose: () => void;
|
||||
isAudioOnly?: boolean;
|
||||
}
|
||||
|
||||
export function QuantumP2PCall({ room, socket, username, onClose, isAudioOnly = false }: QuantumCallProps) {
|
||||
const localVideoRef = useRef<HTMLVideoElement>(null);
|
||||
const remoteVideoRef = useRef<HTMLVideoElement>(null);
|
||||
const pcRef = useRef<RTCPeerConnection | null>(null);
|
||||
const localStreamRef = useRef<MediaStream | null>(null);
|
||||
const remoteStreamRef = useRef<MediaStream | null>(null);
|
||||
|
||||
// Audio Analyser Refs
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const analyserRef = useRef<AnalyserNode | null>(null);
|
||||
const dataArrayRef = useRef<Uint8Array | null>(null);
|
||||
const animationFrameRef = useRef<number>(0);
|
||||
|
||||
const [isMicOn, setIsMicOn] = useState(true);
|
||||
const [isCameraOn, setIsCameraOn] = useState(!isAudioOnly);
|
||||
const [connectionState, setConnectionState] = useState<string>('Membuka Terowongan Kuantum...');
|
||||
|
||||
// Audio Aura Level (0 to 100)
|
||||
const [remoteAudioLevel, setRemoteAudioLevel] = useState<number>(0);
|
||||
|
||||
// Zero-UI Cinematic Mode
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
const idleTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Draggable PiP (Picture in Picture)
|
||||
const [pipPos, setPipPos] = useState({ x: window.innerWidth - 220, y: window.innerHeight - 320 });
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const dragOffset = useRef({ x: 0, y: 0 });
|
||||
|
||||
const resetIdleTimer = useCallback(() => {
|
||||
setShowControls(true);
|
||||
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
||||
idleTimerRef.current = setTimeout(() => setShowControls(false), 3000);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mousemove', resetIdleTimer);
|
||||
resetIdleTimer();
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', resetIdleTimer);
|
||||
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
||||
};
|
||||
}, [resetIdleTimer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
|
||||
// Phase 83: Omni-Relay Inject
|
||||
const configuration = {
|
||||
iceServers: [
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
{ urls: 'stun:stun1.l.google.com:19302' },
|
||||
{
|
||||
urls: 'turn:160.187.143.172:3478',
|
||||
username: 'xcu ULTRA',
|
||||
credential: 'quantum_mesh'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const pc = new RTCPeerConnection(configuration);
|
||||
pcRef.current = pc;
|
||||
|
||||
pc.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
socket.emit('quantum_candidate', {
|
||||
target: getTargetFromRoom(room),
|
||||
sender: username,
|
||||
candidate: event.candidate,
|
||||
room
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
pc.onconnectionstatechange = () => {
|
||||
setConnectionState(pc.connectionState);
|
||||
};
|
||||
|
||||
pc.ontrack = (event) => {
|
||||
if (remoteVideoRef.current && event.streams[0]) {
|
||||
remoteStreamRef.current = event.streams[0];
|
||||
remoteVideoRef.current.srcObject = event.streams[0];
|
||||
setupAudioAnalyser(event.streams[0]);
|
||||
}
|
||||
};
|
||||
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
video: !isAudioOnly ? { width: { ideal: 1280 }, height: { ideal: 720 } } : false,
|
||||
audio: true
|
||||
}).then((stream) => {
|
||||
localStreamRef.current = stream;
|
||||
if (localVideoRef.current && !isAudioOnly) {
|
||||
localVideoRef.current.srcObject = stream;
|
||||
}
|
||||
stream.getTracks().forEach((track) => pc.addTrack(track, stream));
|
||||
createOffer();
|
||||
}).catch(e => {
|
||||
console.error("Gagal mengakses media:", e);
|
||||
setConnectionState("Akses Kamera/Mic Ditolak!");
|
||||
});
|
||||
|
||||
socket.on('quantum_offer_received', async (data: any) => {
|
||||
if (data.caller !== username && pcRef.current) {
|
||||
await pcRef.current.setRemoteDescription(new RTCSessionDescription(data.sdp));
|
||||
const answer = await pcRef.current.createAnswer();
|
||||
await pcRef.current.setLocalDescription(answer);
|
||||
socket.emit('quantum_answer', {
|
||||
target: data.caller,
|
||||
responder: username,
|
||||
sdp: answer,
|
||||
room
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('quantum_answer_received', async (data: any) => {
|
||||
if (data.responder !== username && pcRef.current) {
|
||||
await pcRef.current.setRemoteDescription(new RTCSessionDescription(data.sdp));
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('quantum_candidate_received', async (data: any) => {
|
||||
if (data.sender !== username && pcRef.current) {
|
||||
try {
|
||||
await pcRef.current.addIceCandidate(new RTCIceCandidate(data.candidate));
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('quantum_call_ended_broadcast', (data: any) => {
|
||||
if (data.sender !== username) {
|
||||
handleEndCall();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off('quantum_offer_received');
|
||||
socket.off('quantum_answer_received');
|
||||
socket.off('quantum_candidate_received');
|
||||
socket.off('quantum_call_ended_broadcast');
|
||||
cleanup();
|
||||
};
|
||||
}, [socket, room]);
|
||||
|
||||
const setupAudioAnalyser = (stream: MediaStream) => {
|
||||
try {
|
||||
const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
const analyser = audioCtx.createAnalyser();
|
||||
analyser.fftSize = 256;
|
||||
const source = audioCtx.createMediaStreamSource(stream);
|
||||
source.connect(analyser);
|
||||
|
||||
audioContextRef.current = audioCtx;
|
||||
analyserRef.current = analyser;
|
||||
dataArrayRef.current = new Uint8Array(analyser.frequencyBinCount);
|
||||
|
||||
const updateAura = () => {
|
||||
if (!analyserRef.current || !dataArrayRef.current) return;
|
||||
analyserRef.current.getByteFrequencyData(dataArrayRef.current as any);
|
||||
|
||||
let sum = 0;
|
||||
for (let i = 0; i < dataArrayRef.current.length; i++) {
|
||||
sum += dataArrayRef.current[i];
|
||||
}
|
||||
const average = sum / dataArrayRef.current.length;
|
||||
// Normalize 0-100
|
||||
const level = Math.min(100, Math.round((average / 255) * 100 * 2));
|
||||
setRemoteAudioLevel(level);
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(updateAura);
|
||||
};
|
||||
|
||||
updateAura();
|
||||
} catch (e) {
|
||||
console.error("Audio Context Error", e);
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
|
||||
if (audioContextRef.current) audioContextRef.current.close();
|
||||
if (localStreamRef.current) localStreamRef.current.getTracks().forEach(track => track.stop());
|
||||
if (pcRef.current) pcRef.current.close();
|
||||
};
|
||||
|
||||
const createOffer = async () => {
|
||||
if (!pcRef.current || !socket) return;
|
||||
try {
|
||||
const offer = await pcRef.current.createOffer();
|
||||
await pcRef.current.setLocalDescription(offer);
|
||||
socket.emit('quantum_offer', {
|
||||
target: getTargetFromRoom(room),
|
||||
caller: username,
|
||||
sdp: offer,
|
||||
room
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Offer creation failed", e);
|
||||
}
|
||||
};
|
||||
|
||||
const getTargetFromRoom = (roomId: string) => {
|
||||
if (roomId.startsWith('DM_')) {
|
||||
const users = roomId.replace('DM_', '').split('_');
|
||||
return users.find(u => u !== username) || '';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const toggleMic = () => {
|
||||
if (localStreamRef.current) {
|
||||
localStreamRef.current.getAudioTracks().forEach(track => track.enabled = !isMicOn);
|
||||
setIsMicOn(!isMicOn);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleCamera = () => {
|
||||
if (localStreamRef.current) {
|
||||
localStreamRef.current.getVideoTracks().forEach(track => track.enabled = !isCameraOn);
|
||||
setIsCameraOn(!isCameraOn);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEndCall = () => {
|
||||
if (socket) {
|
||||
socket.emit('quantum_call_ended', { sender: username, room, target: getTargetFromRoom(room) });
|
||||
}
|
||||
cleanup();
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Drag Logic
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
setIsDragging(true);
|
||||
dragOffset.current = {
|
||||
x: e.clientX - pipPos.x,
|
||||
y: e.clientY - pipPos.y
|
||||
};
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (!isDragging) return;
|
||||
setPipPos({
|
||||
x: e.clientX - dragOffset.current.x,
|
||||
y: e.clientY - dragOffset.current.y
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseUp = () => setIsDragging(false);
|
||||
|
||||
// Deep Sapphire Blue Aura Calculation
|
||||
const auraBlur = 20 + (remoteAudioLevel * 0.8);
|
||||
const auraSpread = remoteAudioLevel * 0.5;
|
||||
const boxShadowStyle = `0 0 ${auraBlur}px ${auraSpread}px rgba(15, 82, 186, ${remoteAudioLevel > 10 ? 0.6 : 0.1})`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 z-[100] flex items-center justify-center pointer-events-auto bg-black/70 backdrop-blur-xl overflow-hidden"
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
>
|
||||
{/* Status Bar */}
|
||||
<div className={`absolute top-6 left-6 text-blue-300 text-xs font-mono bg-blue-900/30 px-4 py-2 rounded-full border border-blue-500/20 shadow-[0_0_15px_rgba(15,82,186,0.5)] transition-opacity duration-700 ${showControls ? 'opacity-100' : 'opacity-0'}`}>
|
||||
<span className="animate-pulse mr-2">●</span> {connectionState}
|
||||
</div>
|
||||
|
||||
<div className="w-full h-full relative flex items-center justify-center p-8">
|
||||
|
||||
{/* Main Remote Video (Deep Sapphire Blue Aura) */}
|
||||
<div
|
||||
className="relative w-full max-w-5xl aspect-video rounded-[2rem] overflow-hidden transition-all duration-300"
|
||||
style={{ boxShadow: boxShadowStyle }}
|
||||
>
|
||||
{/* Glassmorphism Border */}
|
||||
<div className="absolute inset-0 rounded-[2rem] border-[1px] border-white/10 z-10 pointer-events-none"></div>
|
||||
<video
|
||||
ref={remoteVideoRef}
|
||||
autoPlay
|
||||
playsInline
|
||||
className="w-full h-full object-cover"
|
||||
style={{ filter: 'contrast(1.05) saturate(1.1)' }}
|
||||
/>
|
||||
|
||||
{/* Fallback Audio Only Avatar */}
|
||||
{isAudioOnly && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-900">
|
||||
<div className="w-32 h-32 rounded-full bg-blue-600/20 flex items-center justify-center" style={{ transform: `scale(${1 + remoteAudioLevel/100})`, transition: 'transform 0.1s ease-out' }}>
|
||||
<span className="text-5xl text-blue-400 font-bold">{getTargetFromRoom(room).charAt(0).toUpperCase()}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Draggable PiP (Local Video) */}
|
||||
<div
|
||||
className="absolute w-48 h-72 rounded-2xl overflow-hidden cursor-move border-[1px] border-white/20 shadow-[0_20px_50px_rgba(0,0,0,0.5)] z-30 transition-shadow hover:shadow-[0_0_20px_rgba(255,255,255,0.2)]"
|
||||
style={{
|
||||
left: pipPos.x, top: pipPos.y,
|
||||
backdropFilter: 'blur(20px)',
|
||||
backgroundColor: 'rgba(15, 23, 42, 0.4)'
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
{isCameraOn ? (
|
||||
<video
|
||||
ref={localVideoRef}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted
|
||||
className="w-full h-full object-cover transform -scale-x-100"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<svg className="w-10 h-10 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2zM3 3l18 18"></path></svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cinematic Controls (Zero-UI) */}
|
||||
<div className={`absolute bottom-10 left-1/2 -translate-x-1/2 flex items-center gap-6 bg-slate-900/60 backdrop-blur-2xl px-10 py-5 rounded-full border border-white/5 shadow-2xl z-40 transition-all duration-700 transform ${showControls ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0 pointer-events-none'}`}>
|
||||
|
||||
{/* Mic Toggle */}
|
||||
<button onClick={toggleMic} className={`relative group w-14 h-14 rounded-full flex items-center justify-center transition-all duration-300 ${isMicOn ? 'bg-white/10 hover:bg-white/20 text-white' : 'bg-red-500/80 text-white shadow-[0_0_20px_rgba(239,68,68,0.4)]'}`}>
|
||||
{isMicOn ? (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5-3c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-1.7z"/></svg>
|
||||
) : (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c-.37-.05-.74-.12-1.1-.22l2.83 2.83 1.27-1.27L4.27 3z"/></svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Camera Toggle */}
|
||||
<button onClick={toggleCamera} className={`relative group w-14 h-14 rounded-full flex items-center justify-center transition-all duration-300 ${isCameraOn ? 'bg-white/10 hover:bg-white/20 text-white' : 'bg-red-500/80 text-white shadow-[0_0_20px_rgba(239,68,68,0.4)]'}`}>
|
||||
{isCameraOn ? (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>
|
||||
) : (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M21 6.5l-4 4V7c0-.55-.45-1-1-1H9.82L21 17.18V6.5zM3.27 2L2 3.27 4.73 6H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 .64-.15 1.14-.39L20.73 22 22 20.73 3.27 2z"/></svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* End Call */}
|
||||
<button onClick={handleEndCall} className="w-16 h-16 rounded-full flex items-center justify-center bg-gradient-to-r from-red-600 to-rose-600 hover:from-red-500 hover:to-rose-500 text-white shadow-[0_0_30px_rgba(225,29,72,0.6)] transition-all duration-300 transform hover:scale-110">
|
||||
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24"><path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08c-.18-.17-.29-.42-.29-.7 0-.28.11-.53.29-.71C3.34 8.78 7.46 7 12 7s8.66 1.78 11.71 4.67c.18.18.29.43.29.71 0 .28-.11.53-.29.71l-2.48 2.48c-.18.18-.43.29-.71.29-.27 0-.52-.11-.7-.28-.79-.74-1.69-1.36-2.67-1.85-.33-.16-.56-.5-.56-.9v-3.1C15.15 9.25 13.6 9 12 9z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { XCUTelepathyMatrix, DecryptedMessage } from "../lib/xcu-telepathy-matrix";
|
||||
|
||||
export function XCUltraChat({ roomName, username }: { roomName: string, username: string }) {
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const [isMatrixActive, setIsMatrixActive] = useState(false);
|
||||
const [chatInput, setChatInput] = useState("");
|
||||
const [chatMessages, setChatMessages] = useState<{id: string, sender: string, text: string, time: string}[]>([]);
|
||||
const [typingUsers, setTypingUsers] = useState<string[]>([]);
|
||||
const [activeReactions, setActiveReactions] = useState<{id: number, type: string, ts: number}[]>([]);
|
||||
|
||||
const chatEndRef = useRef<HTMLDivElement>(null);
|
||||
const matrixRef = useRef<XCUTelepathyMatrix | null>(null);
|
||||
|
||||
const addLog = (msg: string) => {
|
||||
setLogs(prev => [...prev, msg]);
|
||||
};
|
||||
|
||||
const setStatus = (s: string) => addLog(`[SYS] ${s}`);
|
||||
|
||||
useEffect(() => {
|
||||
if (chatEndRef.current) {
|
||||
chatEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [chatMessages, typingUsers]);
|
||||
|
||||
useEffect(() => {
|
||||
let matrix: XCUTelepathyMatrix | null = null;
|
||||
|
||||
const initMatrix = async () => {
|
||||
try {
|
||||
setStatus("Memulai Koneksi XCU Telepathy Matrix (XTM) E2EE...");
|
||||
matrix = new XCUTelepathyMatrix(roomName);
|
||||
matrixRef.current = matrix;
|
||||
|
||||
matrix.onMessagesUpdate = (messages: DecryptedMessage[]) => {
|
||||
// Format time based on timestamp
|
||||
const formatted = messages.map(m => {
|
||||
const date = new Date(m.timestamp);
|
||||
return {
|
||||
id: m.id,
|
||||
sender: m.sender,
|
||||
text: m.content,
|
||||
time: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
};
|
||||
});
|
||||
// CRDT arrays are append-only mostly for chat, but we sync everything
|
||||
setChatMessages(formatted);
|
||||
};
|
||||
|
||||
matrix.onTypingUpdate = (typings: Record<string, number>) => {
|
||||
const active = Object.keys(typings).filter(key => key !== username);
|
||||
setTypingUsers(active);
|
||||
};
|
||||
|
||||
// PKEPX Zoom-Killer Handlers
|
||||
matrix.onQuantumResonance = (id, type) => {
|
||||
setActiveReactions(prev => [...prev, { id, type, ts: Date.now() }]);
|
||||
setTimeout(() => {
|
||||
setActiveReactions(prev => prev.filter(r => Date.now() - r.ts < 4000));
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
matrix.onSovereignSignal = (command, targetId) => {
|
||||
if (command === 'MUTE_ALL') {
|
||||
setStatus("Host telah mengunci matriks komunikasi (Sovereign Lock).");
|
||||
}
|
||||
};
|
||||
|
||||
// Determine server URL dynamically for backup WebSocket
|
||||
const proto = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
let serverUrl = `${proto}//${host}`;
|
||||
if (typeof window === 'undefined' || host === '') {
|
||||
serverUrl = 'http://localhost:4003';
|
||||
}
|
||||
|
||||
matrix.ignite(serverUrl, username);
|
||||
|
||||
setIsMatrixActive(true);
|
||||
setStatus("XTM CRDT Engine Siap. Enkripsi AES-256 Aktif.");
|
||||
} catch (e: any) {
|
||||
console.error("XTM ERROR DETAILS:", e, e.name, e.message);
|
||||
setStatus("Koneksi Gagal: " + e.name + " - " + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
initMatrix();
|
||||
|
||||
return () => {
|
||||
// Dihapus untuk mencegah React 18 race condition yang memotong koneksi QUIC secara prematur
|
||||
if (matrixRef.current) {
|
||||
// matrixRef.current.shutdown();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// (Moved setStatus up)
|
||||
|
||||
const handleSendChat = async (e?: React.FormEvent) => {
|
||||
if (e) e.preventDefault();
|
||||
if (!chatInput.trim() || !matrixRef.current) return;
|
||||
|
||||
const currentInput = chatInput;
|
||||
// XTM Send Message (Automatically encrypted and synced via CRDT)
|
||||
matrixRef.current.sendMessage(username, currentInput);
|
||||
setChatInput("");
|
||||
|
||||
// FASE 87: OMNIBRAIN AVATAR PROTOCOL (CLIENT-SIDE TELEPATHY)
|
||||
if (currentInput.includes('@OmniBrain')) {
|
||||
// The browser acts as the medium for the AI to preserve E2EE
|
||||
matrixRef.current.sendMessage('OmniBrain', 'Sedang mengurai matriks kuantum...');
|
||||
|
||||
try {
|
||||
// Take last 5 decrypted messages for context
|
||||
const recentHistory = chatMessages.slice(-5);
|
||||
|
||||
const res = await fetch('/api/omnibrain', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
history: recentHistory,
|
||||
prompt: currentInput.replace('@OmniBrain', '').trim(),
|
||||
sender: username
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.reply && matrixRef.current) {
|
||||
matrixRef.current.sendMessage('OmniBrain', data.reply);
|
||||
}
|
||||
} catch (err) {
|
||||
if (matrixRef.current) {
|
||||
matrixRef.current.sendMessage('OmniBrain', 'Gagal memanggil entitas di peladen pusat.');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
setChatInput(val);
|
||||
if (matrixRef.current && val.length > 0) {
|
||||
matrixRef.current.setTyping(username, val.length);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-dvh w-full bg-[#0a0c10] text-slate-200 font-sans flex flex-col overflow-hidden relative">
|
||||
{/* Top Banner */}
|
||||
<div className="h-16 border-b border-white/5 bg-[#14161f] flex items-center justify-between px-6 shadow-md z-20 shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-cyan-600 to-blue-700 rounded-xl flex items-center justify-center text-white shadow-[0_0_15px_rgba(8,145,178,0.4)]">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-base font-bold tracking-wider uppercase text-cyan-400">XCU OMNI CHAT</h1>
|
||||
<p className="text-xs text-slate-500 font-mono">{roomName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs font-medium">
|
||||
{/* PKEPX Resonance Button */}
|
||||
<button onClick={() => { if(matrixRef.current) matrixRef.current.emitResonance('👍'); }} className="p-2 bg-yellow-500/10 hover:bg-yellow-500/20 text-yellow-500 rounded-lg transition-colors border border-yellow-500/20 shadow-[0_0_10px_rgba(234,179,8,0.2)]">
|
||||
👍 React
|
||||
</button>
|
||||
<span className="bg-cyan-900/30 text-cyan-400 px-3 py-1.5 rounded-lg border border-cyan-500/30 flex items-center gap-2 shadow-[0_0_10px_rgba(8,145,178,0.2)]">
|
||||
<div className="w-2 h-2 bg-cyan-400 rounded-full animate-pulse"></div>
|
||||
CRDT QUANTUM ENGINE
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PKEPX Floating Emojis */}
|
||||
{activeReactions.map(r => (
|
||||
<div key={r.ts} className="absolute inset-0 flex items-center justify-center pointer-events-none z-[100] animate-[ping_1s_ease-out]">
|
||||
<span className="text-6xl drop-shadow-[0_0_30px_rgba(255,255,255,0.5)] animate-bounce">{r.type}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!isMatrixActive ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-4 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-slate-900 to-[#0a0c10]">
|
||||
<div className="w-16 h-16 border-4 border-slate-800 border-t-cyan-500 rounded-full animate-spin shadow-[0_0_30px_rgba(8,145,178,0.5)]"></div>
|
||||
<div className="text-sm font-mono text-cyan-500 animate-pulse tracking-widest">{logs[logs.length-1] || "INisialisasi..."}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col bg-transparent overflow-hidden">
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6 custom-scroll">
|
||||
{chatMessages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-slate-600 opacity-50">
|
||||
<svg className="w-16 h-16 mb-4 text-cyan-900" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path></svg>
|
||||
<div className="text-center text-sm font-mono tracking-widest uppercase">P2P Kuantum Siap. 0ms Latensi.</div>
|
||||
</div>
|
||||
) : (
|
||||
chatMessages.map(msg => {
|
||||
const isMe = msg.sender === username;
|
||||
return (
|
||||
<div key={msg.id} className={`flex flex-col ${isMe ? 'items-end' : 'items-start'} animate-in fade-in slide-in-from-bottom-2`}>
|
||||
<div className={`px-5 py-3 rounded-2xl max-w-[80%] text-[15px] leading-relaxed shadow-lg ${isMe ? 'bg-gradient-to-br from-cyan-600 to-blue-700 text-white rounded-br-sm shadow-cyan-900/20' : 'bg-[#1a1d24] text-slate-200 border border-white/5 rounded-bl-sm'}`}>
|
||||
<div className="text-[10px] opacity-60 mb-1.5 font-mono tracking-wider uppercase flex items-center gap-2">
|
||||
{isMe ? 'Anda' : msg.sender}
|
||||
<span className="w-1 h-1 bg-white/30 rounded-full"></span>
|
||||
{msg.time}
|
||||
</div>
|
||||
<p className="break-words">{msg.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
{/* FASE 86: XTM Typing Indicator */}
|
||||
{typingUsers.length > 0 && (
|
||||
<div className="text-xs text-cyan-500 font-mono tracking-widest px-2 animate-pulse mt-1 mb-2">
|
||||
{typingUsers.join(", ")} sedang merajut matriks teks...
|
||||
</div>
|
||||
)}
|
||||
<div ref={chatEndRef} />
|
||||
</div>
|
||||
|
||||
<div className="p-5 border-t border-white/5 bg-[#14161f] shrink-0">
|
||||
<form onSubmit={handleSendChat} className="flex items-center gap-3">
|
||||
<div className="flex-1 bg-[#0a0c10] rounded-xl px-5 py-3 border border-white/10 focus-within:border-cyan-500/50 focus-within:shadow-[0_0_15px_rgba(8,145,178,0.2)] transition-all flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
value={chatInput}
|
||||
onChange={handleInputChange}
|
||||
className="flex-1 bg-transparent text-sm text-slate-200 outline-none placeholder:text-slate-600"
|
||||
placeholder="Transmisikan matriks teks E2EE..."
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<button title="Aksi" type="submit" disabled={!chatInput.trim()} className="bg-cyan-600 hover:bg-cyan-500 text-white p-3.5 rounded-xl disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-[0_0_15px_rgba(8,145,178,0.3)] flex items-center justify-center hover:scale-105 active:scale-95">
|
||||
<svg className="w-5 h-5 translate-x-0.5" fill="currentColor" viewBox="0 0 20 20"><path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"></path></svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<style dangerouslySetInnerHTML={{__html: `
|
||||
.custom-scroll::-webkit-scrollbar { width: 6px; }
|
||||
.custom-scroll::-webkit-scrollbar-track { background: transparent; }
|
||||
.custom-scroll::-webkit-scrollbar-thumb { background: rgba(34, 211, 238, 0.2); border-radius: 6px; }
|
||||
.custom-scroll::-webkit-scrollbar-thumb:hover { background: rgba(34, 211, 238, 0.4); }
|
||||
`}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -0,0 +1,15 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import { getRequestConfig } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export const locales = ['id', 'en'] as const;
|
||||
export type Locale = (typeof locales)[number];
|
||||
|
||||
export default getRequestConfig(async ({ locale }) => {
|
||||
if (!locales.includes(locale as Locale)) notFound();
|
||||
|
||||
return {
|
||||
locale: locale as string,
|
||||
messages: (await import(`./messages/${locale}.json`)).default
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
export type SocketCallback = (data: unknown) => void;
|
||||
|
||||
/// XCU Media over QUIC (MoQ) / WebSocket Adapter
|
||||
/// Menggantikan `ZeroSocket` dan `Socket.IO`
|
||||
/// Fitur:
|
||||
/// - Terhubung langsung ke mesin Rust (`xcu-neural-chat`) di port 8443
|
||||
/// - Tidak menggunakan Redis PubSub
|
||||
/// - Berjalan via WebSocket (fallback untuk WebTransport tanpa HTTPS certs)
|
||||
export class XcuMoq {
|
||||
private url: string;
|
||||
private listeners: Record<string, SocketCallback[]> = {};
|
||||
private ws: WebSocket | null = null;
|
||||
public id: string;
|
||||
private reconnectTimer: any;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
this.id = Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) return;
|
||||
|
||||
// Sambungkan langsung ke Alpha VPS atau localhost
|
||||
let wsUrl = "ws://160.187.143.253:8443";
|
||||
if (typeof window !== "undefined" && window.location.hostname === "localhost") {
|
||||
wsUrl = "ws://127.0.0.1:8443";
|
||||
}
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log("[XCU MoQ] Terhubung ke Neural Mesh Rust Engine.");
|
||||
if (this.listeners["connect"]) {
|
||||
this.listeners["connect"].forEach(fn => fn({}));
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onmessage = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.event && this.listeners[data.event]) {
|
||||
this.listeners[data.event].forEach(fn => fn(data.payload));
|
||||
}
|
||||
} catch {
|
||||
// Abaikan parse error
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.warn("[XCU MoQ] Koneksi terputus. Neural link lost.");
|
||||
if (this.listeners["disconnect"]) {
|
||||
this.listeners["disconnect"].forEach(fn => fn({}));
|
||||
}
|
||||
// Auto-reconnect
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = setTimeout(() => this.connect(), 3000);
|
||||
};
|
||||
|
||||
this.ws.onerror = (err) => {
|
||||
console.error("[XCU MoQ] WebSocket Error:", err);
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("[XCU MoQ] Gagal inisialisasi:", e);
|
||||
}
|
||||
}
|
||||
|
||||
on(event: string, callback: SocketCallback) {
|
||||
if (!this.listeners[event]) this.listeners[event] = [];
|
||||
this.listeners[event].push(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
once(event: string, callback: SocketCallback) {
|
||||
const onceWrapper = (data: unknown) => {
|
||||
callback(data);
|
||||
this.off(event, onceWrapper);
|
||||
};
|
||||
this.on(event, onceWrapper);
|
||||
return this;
|
||||
}
|
||||
|
||||
off(event: string, callback?: SocketCallback) {
|
||||
if (!this.listeners[event]) return this;
|
||||
if (callback) {
|
||||
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
|
||||
} else {
|
||||
delete this.listeners[event];
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
emit(event: string, payload: string | Record<string, unknown> = {}) {
|
||||
const normalizedPayload = typeof payload === 'string' ? { value: payload } : payload;
|
||||
|
||||
// Kirim pesan ke mesin Rust via WebSocket
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
const msg = JSON.stringify({
|
||||
client_id: this.id,
|
||||
event,
|
||||
payload: normalizedPayload
|
||||
});
|
||||
this.ws.send(msg);
|
||||
} else {
|
||||
console.warn("[XCU MoQ] Pesan di-drop: Tidak terhubung ke Neural Mesh.");
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
clearTimeout(this.reconnectTimer);
|
||||
}
|
||||
}
|
||||
|
||||
export const io = (url?: string, options?: any) => {
|
||||
const socket = new XcuMoq(url || "");
|
||||
socket.connect();
|
||||
return socket;
|
||||
};
|
||||
|
||||
export type Socket = XcuMoq;
|
||||
@@ -0,0 +1,519 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
export class XCUQuantumMatrix {
|
||||
private transport: WebTransport | null = null;
|
||||
private stream: WebTransportBidirectionalStream | null = null;
|
||||
private streamWriter: WritableStreamDefaultWriter | null = null;
|
||||
private streamReader: ReadableStreamDefaultReader | null = null;
|
||||
private videoDecoders: Map<number, VideoDecoder> = new Map();
|
||||
private canvasCtxMap: Map<number, CanvasRenderingContext2D> = new Map();
|
||||
private videoEncoder: VideoEncoder | null = null;
|
||||
private isRunning: boolean = false;
|
||||
private mediaStream: MediaStream | null = null;
|
||||
public participantId: number = 0;
|
||||
public participantRole: "PANELIST" | "AUDIENCE" = "PANELIST";
|
||||
|
||||
// Callbacks
|
||||
public onParticipantJoined: ((id: number) => void) | null = null;
|
||||
public onParticipantLeft: ((id: number) => void) | null = null;
|
||||
public onQuantumDataReceived:
|
||||
| ((senderId: number, payload: string) => void)
|
||||
| null = null;
|
||||
public onLocalStream: ((stream: MediaStream) => void) | null = null;
|
||||
public onActiveSpeakerChanged: ((speakerId: number) => void) | null = null;
|
||||
public onAudioLevel: ((level: number) => void) | null = null;
|
||||
|
||||
private vadAudioCtx: AudioContext | null = null;
|
||||
|
||||
public registerCanvas(participantId: number, canvasId: string) {
|
||||
const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) this.canvasCtxMap.set(participantId, ctx);
|
||||
console.log(
|
||||
`[QUANTUM MATRIX] Canvas didaftarkan untuk Partisipan ${participantId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async ignite() {
|
||||
this.isRunning = true;
|
||||
console.log(
|
||||
"[QUANTUM MATRIX] Menginisialisasi Pipa WebTransport & WebCodecs...",
|
||||
);
|
||||
|
||||
// 2. Setup WebTransport ke Rust Backend
|
||||
try {
|
||||
// Menggunakan port Standar Kuantum 4433 (UDP) dengan Sertifikat Valid Let's Encrypt
|
||||
this.transport = new WebTransport(
|
||||
typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.hostname}:8443` : "/xcu-engine",
|
||||
);
|
||||
await this.transport.ready;
|
||||
console.log("[QUANTUM MATRIX] Terhubung ke Server Zero-Copy QUIC!");
|
||||
|
||||
// Setup awal dihapus, decoder akan dibuat per-partisipan
|
||||
this.startReceiver();
|
||||
} catch (e: unknown) {
|
||||
console.error("[QUANTUM MATRIX] Gagal menembus matriks QUIC:", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public toggleMic(enabled: boolean) {
|
||||
if (this.mediaStream) {
|
||||
this.mediaStream.getAudioTracks().forEach((track) => {
|
||||
track.enabled = enabled;
|
||||
});
|
||||
console.log(`[QUANTUM UPLINK] Mikrofon ${enabled ? "Aktif" : "Mati"}`);
|
||||
}
|
||||
}
|
||||
|
||||
public resumeAudioContext() {
|
||||
if (this.vadAudioCtx && this.vadAudioCtx.state === "suspended") {
|
||||
this.vadAudioCtx
|
||||
.resume()
|
||||
.catch((e) => console.error("Force resume failed", e));
|
||||
}
|
||||
}
|
||||
|
||||
private createDecoderForParticipant(senderId: number) {
|
||||
const decoder = new VideoDecoder({
|
||||
output: (frame: unknown) => {
|
||||
const ctx = this.canvasCtxMap.get(senderId);
|
||||
if (ctx) {
|
||||
ctx.drawImage(
|
||||
frame as unknown as CanvasImageSource,
|
||||
0,
|
||||
0,
|
||||
ctx.canvas.width,
|
||||
ctx.canvas.height,
|
||||
);
|
||||
}
|
||||
(frame as { close(): void }).close();
|
||||
},
|
||||
error: (e: unknown) =>
|
||||
console.error(`Decoder Error untuk ${senderId}:`, e),
|
||||
});
|
||||
|
||||
decoder.configure({
|
||||
codec: "avc1.42E01E", // H.264 Baseline
|
||||
codedWidth: 1280,
|
||||
codedHeight: 720,
|
||||
});
|
||||
|
||||
this.videoDecoders.set(senderId, decoder);
|
||||
|
||||
// Notifikasi React untuk merender tile
|
||||
if (this.onParticipantJoined) {
|
||||
this.onParticipantJoined(senderId);
|
||||
}
|
||||
}
|
||||
|
||||
public async activateUplink(source: "camera" | "screen" = "camera") {
|
||||
if (this.mediaStream) {
|
||||
console.warn(
|
||||
"[QUANTUM UPLINK] Kamera/Layar sudah aktif! Mengabaikan perintah ganda untuk mencegah kebocoran memori.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
`[QUANTUM UPLINK] Memulai Injeksi ${source === "screen" ? "Layar" : "Kamera"} ke WebCodecs...`,
|
||||
);
|
||||
try {
|
||||
if (source === "screen") {
|
||||
this.mediaStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: { width: 1280, height: 720 },
|
||||
audio: false,
|
||||
});
|
||||
} else {
|
||||
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { width: 1280, height: 720 },
|
||||
audio: true,
|
||||
});
|
||||
this.startVadLoop(this.mediaStream); // Phase 37: Auto-Start VAD
|
||||
}
|
||||
|
||||
if (this.onLocalStream) {
|
||||
this.onLocalStream(this.mediaStream);
|
||||
}
|
||||
|
||||
// Jika Klien adalah AUDIENCE, abaikan penyalaan WebCodecs untuk menghemat RAM 100%
|
||||
if (this.participantRole === "AUDIENCE") {
|
||||
console.log(
|
||||
"[QUANTUM MATRIX] Tersambung sebagai AUDIENCE. Menutup jalur Uplink Video.",
|
||||
);
|
||||
await this.sendRoleSignal();
|
||||
return;
|
||||
}
|
||||
|
||||
const videoTrack = this.mediaStream.getVideoTracks()[0];
|
||||
|
||||
const processor = new MediaStreamTrackProcessor({
|
||||
track: videoTrack,
|
||||
});
|
||||
const reader = processor.readable.getReader();
|
||||
|
||||
// Setup Hardware Encoder
|
||||
this.videoEncoder = new VideoEncoder({
|
||||
output: async (chunk: unknown) => {
|
||||
if (this.streamWriter && this.isRunning) {
|
||||
// Quantum Protocol v3: 8-Byte Header
|
||||
// Byte 0: Type (0=Delta, 1=Key, 2=Audio, 3=Control)
|
||||
// Byte 1: Quality (0=Low, 1=High, 2=VAD Active)
|
||||
// Byte 2-3: Participant ID (u16)
|
||||
// Byte 4-7: Length (4 bytes)
|
||||
const packet = new Uint8Array(
|
||||
8 + (chunk as { byteLength: number }).byteLength,
|
||||
);
|
||||
packet[0] = (chunk as { type: string }).type === "key" ? 1 : 0;
|
||||
packet[1] = 1; // High Quality
|
||||
|
||||
const view = new DataView(packet.buffer);
|
||||
view.setUint16(2, this.participantId, true);
|
||||
view.setUint32(
|
||||
4,
|
||||
(chunk as { byteLength: number }).byteLength,
|
||||
true,
|
||||
); // Little-endian length
|
||||
|
||||
(chunk as { copyTo(buf: ArrayBuffer): void }).copyTo(
|
||||
packet.buffer.slice(8),
|
||||
);
|
||||
|
||||
// Tembakkan via QUIC Stream (Otomatis Fragmentasi MTU)
|
||||
await this.streamWriter.write(packet);
|
||||
}
|
||||
},
|
||||
error: (e: unknown) => console.error("Encoder Error:", e),
|
||||
});
|
||||
|
||||
this.videoEncoder.configure({
|
||||
codec: "avc1.42E01E", // H.264
|
||||
width: 1280,
|
||||
height: 720,
|
||||
bitrate: 2_000_000, // 2 Mbps
|
||||
framerate: 30,
|
||||
latencyMode: "realtime",
|
||||
});
|
||||
|
||||
// Loop pengiriman Frame ke GPU Encoder
|
||||
this.encodeLoop(reader);
|
||||
} catch (e: unknown) {
|
||||
console.error("[QUANTUM UPLINK] Gagal mengaktifkan kamera:", e);
|
||||
}
|
||||
}
|
||||
|
||||
public async deactivateUplink() {
|
||||
console.log("[QUANTUM UPLINK] Mematikan Kamera (Downlink tetap aktif)...");
|
||||
if (this.mediaStream) {
|
||||
this.mediaStream.getTracks().forEach((t) => {
|
||||
t.stop();
|
||||
});
|
||||
this.mediaStream = null;
|
||||
}
|
||||
if (this.videoEncoder && this.videoEncoder.state !== "closed") {
|
||||
this.videoEncoder.close();
|
||||
this.videoEncoder = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async encodeLoop(reader: ReadableStreamDefaultReader) {
|
||||
while (this.isRunning) {
|
||||
try {
|
||||
const { done, value: frame } = await reader.read();
|
||||
if (done || !frame) break;
|
||||
|
||||
if (this.videoEncoder && this.videoEncoder.state === "configured") {
|
||||
this.videoEncoder.encode(frame, { keyFrame: false });
|
||||
}
|
||||
if (frame) frame.close();
|
||||
} catch {
|
||||
// Stream terputus atau kamera dimatikan
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async startReceiver() {
|
||||
if (!this.transport) return;
|
||||
|
||||
try {
|
||||
console.log("[QUANTUM MATRIX] Menciptakan QUIC Bidirectional Stream...");
|
||||
// Klien yang berinisiatif membuka Bi-Directional Stream pertama kali
|
||||
this.stream = await this.transport.createBidirectionalStream();
|
||||
this.streamWriter = this.stream.writable.getWriter();
|
||||
this.streamReader = this.stream.readable.getReader();
|
||||
|
||||
let buffer = new Uint8Array(0);
|
||||
|
||||
while (this.isRunning) {
|
||||
const { value, done } = await this.streamReader.read();
|
||||
if (done) {
|
||||
console.log("QUIC Stream Ditutup oleh Server.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
// Gabungkan chunk yang baru datang ke dalam buffer
|
||||
const newBuffer = new Uint8Array(buffer.length + value.length);
|
||||
newBuffer.set(buffer);
|
||||
newBuffer.set(value, buffer.length);
|
||||
buffer = newBuffer;
|
||||
|
||||
// Ekstrak Frame berdasarkan 8-byte header
|
||||
while (buffer.length >= 8) {
|
||||
const frameType = buffer[0];
|
||||
const quality = buffer[1];
|
||||
const view = new DataView(
|
||||
buffer.buffer,
|
||||
buffer.byteOffset,
|
||||
buffer.byteLength,
|
||||
);
|
||||
const senderId = view.getUint16(2, true);
|
||||
const frameLength = view.getUint32(4, true);
|
||||
|
||||
if (buffer.length >= 8 + frameLength) {
|
||||
const payloadData = buffer.slice(8, 8 + frameLength);
|
||||
buffer = buffer.slice(8 + frameLength);
|
||||
|
||||
// [FASE 40] Eksekusi Quantum Ledger (Telepati Data)
|
||||
if (frameType === 6) {
|
||||
if (this.onQuantumDataReceived) {
|
||||
const textDecoder = new TextDecoder();
|
||||
const payloadStr = textDecoder.decode(payloadData);
|
||||
this.onQuantumDataReceived(senderId, payloadStr);
|
||||
}
|
||||
continue; // Lanjut ke paket berikutnya, ini bukan Video
|
||||
}
|
||||
|
||||
// [FASE 37] Tangkap Sinyal Active Speaker Broadcast dari Core
|
||||
if (frameType === 3 && quality === 2) {
|
||||
if (this.onActiveSpeakerChanged) {
|
||||
this.onActiveSpeakerChanged(senderId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Injeksi Frame Video langsung ke Hardware Decoder Klien
|
||||
if (frameType === 0 || frameType === 1) {
|
||||
if (!this.videoDecoders.has(senderId)) {
|
||||
this.createDecoderForParticipant(senderId);
|
||||
}
|
||||
|
||||
const decoder = this.videoDecoders.get(senderId);
|
||||
if (decoder && decoder.state === "configured") {
|
||||
const chunk = new EncodedVideoChunk({
|
||||
type: frameType === 1 ? "key" : "delta",
|
||||
timestamp: performance.now() * 1000,
|
||||
data: payloadData,
|
||||
});
|
||||
decoder.decode(chunk);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Butuh lebih banyak byte untuk membentuk frame utuh
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
console.error("[QUANTUM MATRIX] Kegagalan Aliran (Stream Failure):", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fase 37: Voice Activity Detection (VAD) Loop
|
||||
private startVadLoop(stream: MediaStream) {
|
||||
try {
|
||||
this.vadAudioCtx = new (
|
||||
window.AudioContext ||
|
||||
(window as unknown as { webkitAudioContext: typeof AudioContext })
|
||||
.webkitAudioContext
|
||||
)();
|
||||
if (this.vadAudioCtx.state === "suspended") {
|
||||
this.vadAudioCtx
|
||||
.resume()
|
||||
.catch((e) => console.warn("[VAD] Cannot resume AudioContext", e));
|
||||
}
|
||||
const source = this.vadAudioCtx.createMediaStreamSource(stream);
|
||||
const analyser = this.vadAudioCtx.createAnalyser();
|
||||
analyser.fftSize = 512;
|
||||
|
||||
// Trik khusus untuk browser modern agar benar-benar memproses FFT stream
|
||||
const dummyGain = this.vadAudioCtx.createGain();
|
||||
dummyGain.gain.value = 0; // Bisukan (Mute) agar tidak terjadi echo
|
||||
|
||||
source.connect(analyser);
|
||||
analyser.connect(dummyGain);
|
||||
dummyGain.connect(this.vadAudioCtx.destination);
|
||||
|
||||
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
||||
let speakingFrames = 0;
|
||||
|
||||
setInterval(() => {
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
const sum = dataArray.reduce((a, b) => a + b, 0);
|
||||
const avg = sum / dataArray.length;
|
||||
|
||||
if (this.onAudioLevel) {
|
||||
this.onAudioLevel(avg);
|
||||
}
|
||||
|
||||
if (avg > 5) {
|
||||
// Ambang batas diturunkan drastis ke 5
|
||||
speakingFrames++;
|
||||
if (speakingFrames === 3) {
|
||||
// Hanya kirim jika uplink siap
|
||||
if (this.streamWriter && this.isRunning) {
|
||||
this.sendDirectorSignal(2);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
speakingFrames = 0;
|
||||
}
|
||||
}, 100);
|
||||
} catch (e: unknown) {
|
||||
console.warn(
|
||||
"[VAD] Audio Context tidak didukung atau dicekal browser.",
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendDirectorSignal(actionQuality: number) {
|
||||
if (!this.streamWriter) return;
|
||||
const packet = new Uint8Array(8);
|
||||
packet[0] = 3; // Control
|
||||
packet[1] = actionQuality;
|
||||
const view = new DataView(packet.buffer);
|
||||
view.setUint16(2, this.participantId, true);
|
||||
view.setUint32(4, 0, true);
|
||||
await this.streamWriter.write(packet);
|
||||
}
|
||||
|
||||
// Fase 34: Zero-CPU Omniscient Recording
|
||||
public async triggerQuantumRecording() {
|
||||
if (!this.streamWriter) return;
|
||||
const packet = new Uint8Array(8);
|
||||
packet[0] = 3; // Control
|
||||
packet[1] = 6; // Set Action: Record
|
||||
const view = new DataView(packet.buffer);
|
||||
view.setUint16(2, this.participantId, true);
|
||||
view.setUint32(4, 0, true);
|
||||
await this.streamWriter.write(packet);
|
||||
console.log(
|
||||
"[VAULT MATRIX] Sinyal Perekaman Kuantum Langsung Ke SSD Server Dikirim.",
|
||||
);
|
||||
}
|
||||
|
||||
// Fase 35: SVC Downgrade (Anti-Lag Manual/Otomatis)
|
||||
public async activateAntiLagDowngrade() {
|
||||
if (this.streamWriter) {
|
||||
console.log("[SVC MATRIX] Mengirim sinyal Downgrade ke Server Rust...");
|
||||
const controlPacket = new Uint8Array(8);
|
||||
controlPacket[0] = 3; // Type: Control
|
||||
controlPacket[1] = 0; // Quality: Low (Drop paket 1080p)
|
||||
const view = new DataView(controlPacket.buffer);
|
||||
view.setUint16(2, this.participantId, true);
|
||||
view.setUint32(4, 0, true); // Payload Length: 0
|
||||
|
||||
await this.streamWriter.write(controlPacket);
|
||||
}
|
||||
}
|
||||
|
||||
// Fase 39: Webinar Asimetris (Audience)
|
||||
public async joinAsAudience() {
|
||||
this.participantRole = "AUDIENCE";
|
||||
await this.sendRoleSignal();
|
||||
console.log(
|
||||
"[WEBINAR MATRIX] Beralih ke Mode AUDIENCE (Uplink Dimatikan).",
|
||||
);
|
||||
}
|
||||
|
||||
private async sendRoleSignal() {
|
||||
if (!this.streamWriter) return;
|
||||
const packet = new Uint8Array(8);
|
||||
packet[0] = 3; // Control
|
||||
packet[1] = 4; // Set Role
|
||||
const view = new DataView(packet.buffer);
|
||||
view.setUint16(2, this.participantId, true);
|
||||
packet[4] = this.participantRole === "AUDIENCE" ? 1 : 0; // 1=Audience, 0=Panelist
|
||||
await this.streamWriter.write(packet);
|
||||
}
|
||||
|
||||
// Fase 39: Podcast Mode (Matikan Video di Routing Level)
|
||||
public async setPodcastMode(isPodcast: boolean) {
|
||||
if (!this.streamWriter) return;
|
||||
const packet = new Uint8Array(8);
|
||||
packet[0] = 3; // Control
|
||||
packet[1] = 5; // Set Mode
|
||||
const view = new DataView(packet.buffer);
|
||||
view.setUint16(2, this.participantId, true);
|
||||
packet[4] = isPodcast ? 1 : 0; // 1=Podcast, 0=Webinar
|
||||
await this.streamWriter.write(packet);
|
||||
console.log(
|
||||
`[PODCAST MATRIX] Sinyal ${isPodcast ? "PODCAST" : "WEBINAR"} dikirim ke Core.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Fase 40: The Quantum Ledger (Pengganti WebSocket Node.js)
|
||||
// Menembakkan Chat, JSON, Emoji, Koordinat langsung via QUIC
|
||||
public async sendQuantumData(payload: string) {
|
||||
if (!this.streamWriter) return;
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const payloadBytes = textEncoder.encode(payload);
|
||||
|
||||
const packet = new Uint8Array(8 + payloadBytes.byteLength);
|
||||
packet[0] = 6; // Type: Ledger Data
|
||||
packet[1] = 0; // Quality: N/A
|
||||
const view = new DataView(packet.buffer);
|
||||
view.setUint16(2, this.participantId, true);
|
||||
view.setUint32(4, payloadBytes.byteLength, true);
|
||||
|
||||
packet.set(payloadBytes, 8);
|
||||
|
||||
await this.streamWriter.write(packet);
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
this.isRunning = false;
|
||||
if (this.transport) {
|
||||
this.transport.close();
|
||||
}
|
||||
this.videoDecoders.forEach((decoder) => {
|
||||
if (decoder.state !== "closed") decoder.close();
|
||||
});
|
||||
this.videoDecoders.clear();
|
||||
this.canvasCtxMap.clear();
|
||||
if (this.videoEncoder && this.videoEncoder.state !== "closed") {
|
||||
this.videoEncoder.close();
|
||||
}
|
||||
if (this.mediaStream) {
|
||||
this.mediaStream.getTracks().forEach((t) => {
|
||||
t.stop();
|
||||
});
|
||||
}
|
||||
console.log("[QUANTUM MATRIX] Sistem Terputus.");
|
||||
}
|
||||
|
||||
public shutdown() {
|
||||
this.isRunning = false;
|
||||
if (this.transport) {
|
||||
this.transport.close();
|
||||
}
|
||||
this.videoDecoders.forEach((decoder) => {
|
||||
if (decoder.state !== "closed") decoder.close();
|
||||
});
|
||||
this.videoDecoders.clear();
|
||||
this.canvasCtxMap.clear();
|
||||
if (this.videoEncoder && this.videoEncoder.state !== "closed") {
|
||||
this.videoEncoder.close();
|
||||
}
|
||||
if (this.mediaStream) {
|
||||
this.mediaStream.getTracks().forEach((t) => {
|
||||
t.stop();
|
||||
});
|
||||
}
|
||||
console.log("[QUANTUM MATRIX] Sistem Terputus.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
export interface EncryptedMessage {
|
||||
id: string;
|
||||
sender: string;
|
||||
ciphertext: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface DecryptedMessage {
|
||||
id: string;
|
||||
sender: string;
|
||||
content: string;
|
||||
timestamp: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* XCUTelepathyMatrix FASE 2:
|
||||
* Telah berevolusi meninggalkan NodeJS, Yjs, dan WebSockets usang.
|
||||
* Menggunakan arsitektur WebTransport Datagrams yang menembak langsung ke Rust Engine (Port 8443).
|
||||
*/
|
||||
export class XCUTelepathyMatrix {
|
||||
private roomName: string;
|
||||
private secretKey: string;
|
||||
private transport: any = null; // WebTransport instance
|
||||
private isActive: boolean = false;
|
||||
private participantId: number;
|
||||
private username: string = "";
|
||||
|
||||
public onMessagesUpdate: ((messages: DecryptedMessage[]) => void) | null = null;
|
||||
public onTypingUpdate: ((typingUsers: Record<string, number>) => void) | null = null;
|
||||
public onQuantumResonance: ((senderId: number, type: string) => void) | null = null;
|
||||
public onSovereignSignal: ((command: string, targetId?: number) => void) | null = null;
|
||||
|
||||
// Local state untuk dirender
|
||||
private messages: DecryptedMessage[] = [];
|
||||
private typingState: Record<string, number> = {};
|
||||
|
||||
constructor(roomName: string, secretKey: string = 'QUANTUM-X-SECRET-256') {
|
||||
this.roomName = roomName;
|
||||
this.secretKey = secretKey;
|
||||
this.participantId = Math.floor(Math.random() * 65534) + 1;
|
||||
}
|
||||
|
||||
public async ignite(serverUrl: string, username: string) {
|
||||
this.username = username;
|
||||
this.isActive = true;
|
||||
|
||||
try {
|
||||
// 1. Ekstrak host dari serverUrl
|
||||
let host = window.location.hostname;
|
||||
if (serverUrl && serverUrl !== "/") {
|
||||
const urlObj = new URL(serverUrl);
|
||||
host = urlObj.hostname;
|
||||
}
|
||||
|
||||
const secureProto = window.location.protocol;
|
||||
const wtUrl = `${secureProto}//${host}:8443/neural-link/${this.roomName}`;
|
||||
console.log("[XTM] Menginisialisasi WebTransport ke:", wtUrl);
|
||||
|
||||
// 2. Setup WebTransport
|
||||
// Gunakan any cast karena WebTransport mungkin belum diakui di semua tsconfig
|
||||
const WT = (window as any).WebTransport;
|
||||
if (!WT) {
|
||||
throw new Error("WebTransport tidak didukung di browser ini!");
|
||||
}
|
||||
|
||||
this.transport = new WT(wtUrl);
|
||||
await this.transport.ready;
|
||||
console.log("[XTM] Pipa WebTransport Kuantum TERHUBUNG!");
|
||||
|
||||
// 3. Mulai Membaca Datagrams
|
||||
this.readDatagrams();
|
||||
|
||||
} catch (e) {
|
||||
console.error("[XTM] Ignition Failed:", e);
|
||||
// Fallback: Jika WebTransport diblokir firewall, XTM akan mengaktifkan SSE Relay (TBD)
|
||||
}
|
||||
}
|
||||
|
||||
private async readDatagrams() {
|
||||
if (!this.transport || !this.transport.datagrams) return;
|
||||
|
||||
try {
|
||||
const reader = this.transport.datagrams.readable.getReader();
|
||||
while (this.isActive) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
if (value && value.length >= 8) {
|
||||
const type = value[0];
|
||||
// byte 2-3 = participantId pengirim
|
||||
const senderId = new DataView(value.buffer).getUint16(2, true);
|
||||
|
||||
if (senderId === this.participantId) continue;
|
||||
|
||||
const payload = value.slice(8);
|
||||
|
||||
if (type === 7) { // 7 = Chat Text
|
||||
this.decryptAndPush(payload);
|
||||
} else if (type === 8) { // 8 = Telepathic Resonance (Typing)
|
||||
// Mendekripsi siapa yang mengetik
|
||||
this.handleTypingResonance(payload);
|
||||
} else if (type === 9) { // 9 = PKEPX Resonance (Emoji)
|
||||
this.handleQuantumResonance(payload, senderId);
|
||||
} else if (type === 10) { // 10 = PKEPX Sovereign Command
|
||||
this.handleSovereignCommand(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.isActive) console.error("[XTM] Datagram Reader terputus:", e);
|
||||
}
|
||||
}
|
||||
|
||||
private async decryptAndPush(payload: Uint8Array) {
|
||||
try {
|
||||
// Convert Uint8Array to WordArray
|
||||
const wordArr = CryptoJS.lib.WordArray.create(payload as any);
|
||||
const ciphertext = CryptoJS.enc.Base64.stringify(wordArr);
|
||||
|
||||
const bytes = CryptoJS.AES.decrypt(ciphertext, this.secretKey);
|
||||
const originalText = bytes.toString(CryptoJS.enc.Utf8);
|
||||
|
||||
if (originalText) {
|
||||
const parsed = JSON.parse(originalText);
|
||||
this.messages.push({
|
||||
id: Math.random().toString(),
|
||||
sender: parsed.sender,
|
||||
content: parsed.text,
|
||||
timestamp: parsed.timestamp,
|
||||
status: 'delivered'
|
||||
});
|
||||
|
||||
if (this.onMessagesUpdate) {
|
||||
this.onMessagesUpdate([...this.messages]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Gagal dekripsi pesan XTM", e);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleTypingResonance(payload: Uint8Array) {
|
||||
try {
|
||||
const wordArr = CryptoJS.lib.WordArray.create(payload as any);
|
||||
const ciphertext = CryptoJS.enc.Base64.stringify(wordArr);
|
||||
const bytes = CryptoJS.AES.decrypt(ciphertext, this.secretKey);
|
||||
const name = bytes.toString(CryptoJS.enc.Utf8);
|
||||
|
||||
if (name) {
|
||||
this.typingState[name] = Date.now();
|
||||
if (this.onTypingUpdate) this.onTypingUpdate({...this.typingState});
|
||||
|
||||
setTimeout(() => {
|
||||
delete this.typingState[name];
|
||||
if (this.onTypingUpdate) this.onTypingUpdate({...this.typingState});
|
||||
}, 3000);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// PKEPX ZOOM-KILLER: JUMPA CHAT PORT
|
||||
// =====================================
|
||||
|
||||
private async handleQuantumResonance(payload: Uint8Array, senderId: number) {
|
||||
try {
|
||||
const wordArr = CryptoJS.lib.WordArray.create(payload as any);
|
||||
const ciphertext = CryptoJS.enc.Base64.stringify(wordArr);
|
||||
const bytes = CryptoJS.AES.decrypt(ciphertext, this.secretKey);
|
||||
const reactionType = bytes.toString(CryptoJS.enc.Utf8);
|
||||
if (reactionType && this.onQuantumResonance) {
|
||||
this.onQuantumResonance(senderId, reactionType);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
private async handleSovereignCommand(payload: Uint8Array) {
|
||||
try {
|
||||
const wordArr = CryptoJS.lib.WordArray.create(payload as any);
|
||||
const ciphertext = CryptoJS.enc.Base64.stringify(wordArr);
|
||||
const bytes = CryptoJS.AES.decrypt(ciphertext, this.secretKey);
|
||||
const cmdStr = bytes.toString(CryptoJS.enc.Utf8);
|
||||
if (cmdStr && this.onSovereignSignal) {
|
||||
const cmd = JSON.parse(cmdStr);
|
||||
this.onSovereignSignal(cmd.command, cmd.targetId);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
public async emitResonance(reactionType: string) {
|
||||
if (!this.transport || !this.transport.datagrams) return;
|
||||
|
||||
const ciphertext = CryptoJS.AES.encrypt(reactionType, this.secretKey).toString();
|
||||
const encWordArr = CryptoJS.enc.Base64.parse(ciphertext);
|
||||
const encPayload = new Uint8Array(encWordArr.sigBytes);
|
||||
for (let i = 0; i < encWordArr.sigBytes; i++) {
|
||||
encPayload[i] = (encWordArr.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||||
}
|
||||
|
||||
const header = new Uint8Array(8);
|
||||
header[0] = 9; // Tipe 9 = Resonance
|
||||
new DataView(header.buffer).setUint16(2, this.participantId, true);
|
||||
|
||||
const fullPacket = new Uint8Array(8 + encPayload.length);
|
||||
fullPacket.set(header, 0);
|
||||
fullPacket.set(encPayload, 8);
|
||||
|
||||
let writer: any = null;
|
||||
try {
|
||||
writer = this.transport.datagrams.writable.getWriter();
|
||||
await writer.write(fullPacket);
|
||||
if (this.onQuantumResonance) this.onQuantumResonance(this.participantId, reactionType);
|
||||
} catch (e) {} finally {
|
||||
if (writer) writer.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
public async broadcastSovereignSignal(command: string, targetId?: number) {
|
||||
if (!this.transport || !this.transport.datagrams) return;
|
||||
|
||||
const payloadStr = JSON.stringify({ command, targetId });
|
||||
const ciphertext = CryptoJS.AES.encrypt(payloadStr, this.secretKey).toString();
|
||||
const encWordArr = CryptoJS.enc.Base64.parse(ciphertext);
|
||||
const encPayload = new Uint8Array(encWordArr.sigBytes);
|
||||
for (let i = 0; i < encWordArr.sigBytes; i++) {
|
||||
encPayload[i] = (encWordArr.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||||
}
|
||||
|
||||
const header = new Uint8Array(8);
|
||||
header[0] = 10; // Tipe 10 = Sovereign
|
||||
new DataView(header.buffer).setUint16(2, this.participantId, true);
|
||||
|
||||
const fullPacket = new Uint8Array(8 + encPayload.length);
|
||||
fullPacket.set(header, 0);
|
||||
fullPacket.set(encPayload, 8);
|
||||
|
||||
let writer: any = null;
|
||||
try {
|
||||
writer = this.transport.datagrams.writable.getWriter();
|
||||
await writer.write(fullPacket);
|
||||
} catch (e) {} finally {
|
||||
if (writer) writer.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt and send
|
||||
public async sendMessage(sender: string, content: string) {
|
||||
if (!this.transport || !this.transport.datagrams) {
|
||||
// Fallback simpan ke local buffer jika belum konek
|
||||
this.messages.push({
|
||||
id: Math.random().toString(),
|
||||
sender,
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
status: 'failed_offline'
|
||||
});
|
||||
if (this.onMessagesUpdate) this.onMessagesUpdate([...this.messages]);
|
||||
return;
|
||||
}
|
||||
|
||||
const payloadStr = JSON.stringify({ sender, text: content, timestamp: Date.now() });
|
||||
|
||||
// E2EE: AES Encryption
|
||||
const ciphertext = CryptoJS.AES.encrypt(payloadStr, this.secretKey).toString();
|
||||
const encWordArr = CryptoJS.enc.Base64.parse(ciphertext);
|
||||
// Convert WordArray to Uint8Array
|
||||
const encPayload = new Uint8Array(encWordArr.sigBytes);
|
||||
for (let i = 0; i < encWordArr.sigBytes; i++) {
|
||||
encPayload[i] = (encWordArr.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||||
}
|
||||
|
||||
// Header
|
||||
const header = new Uint8Array(8);
|
||||
header[0] = 7; // Tipe 7 = Text Message
|
||||
header[1] = 0;
|
||||
new DataView(header.buffer).setUint16(2, this.participantId, true);
|
||||
|
||||
const fullPacket = new Uint8Array(8 + encPayload.length);
|
||||
fullPacket.set(header, 0);
|
||||
fullPacket.set(encPayload, 8);
|
||||
|
||||
let writer: any = null;
|
||||
try {
|
||||
writer = this.transport.datagrams.writable.getWriter();
|
||||
await writer.write(fullPacket);
|
||||
|
||||
// Optimistic UI Update
|
||||
this.messages.push({
|
||||
id: Math.random().toString(),
|
||||
sender,
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
status: 'delivered'
|
||||
});
|
||||
if (this.onMessagesUpdate) this.onMessagesUpdate([...this.messages]);
|
||||
} catch (e) {
|
||||
console.error("Gagal mengirim pesan Kuantum:", e);
|
||||
} finally {
|
||||
if (writer) writer.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
public async setTyping(username: string, charCount: number) {
|
||||
if (!this.transport || !this.transport.datagrams) return;
|
||||
|
||||
const ciphertext = CryptoJS.AES.encrypt(username, this.secretKey).toString();
|
||||
const encWordArr = CryptoJS.enc.Base64.parse(ciphertext);
|
||||
const encPayload = new Uint8Array(encWordArr.sigBytes);
|
||||
for (let i = 0; i < encWordArr.sigBytes; i++) {
|
||||
encPayload[i] = (encWordArr.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||||
}
|
||||
|
||||
const header = new Uint8Array(8);
|
||||
header[0] = 8; // Tipe 8 = Typing
|
||||
new DataView(header.buffer).setUint16(2, this.participantId, true);
|
||||
|
||||
const fullPacket = new Uint8Array(8 + encPayload.length);
|
||||
fullPacket.set(header, 0);
|
||||
fullPacket.set(encPayload, 8);
|
||||
|
||||
let writer: any = null;
|
||||
try {
|
||||
writer = this.transport.datagrams.writable.getWriter();
|
||||
await writer.write(fullPacket);
|
||||
} catch (e) {} finally {
|
||||
if (writer) writer.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
public shutdown() {
|
||||
this.isActive = false;
|
||||
if (this.transport) {
|
||||
try { this.transport.close(); } catch(e){}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
export class XCUWasmLoader {
|
||||
private static instance: XCUWasmLoader;
|
||||
private isLoaded: boolean = false;
|
||||
private isInitializing: boolean = false;
|
||||
private matrixHacked: boolean = false;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): XCUWasmLoader {
|
||||
if (!XCUWasmLoader.instance) {
|
||||
XCUWasmLoader.instance = new XCUWasmLoader();
|
||||
}
|
||||
return XCUWasmLoader.instance;
|
||||
}
|
||||
|
||||
public async injectQuantumSDK(roomName: string, token: string, serverUrl: string, onLog: (msg: string) => void): Promise<boolean> {
|
||||
if (this.isLoaded) return true;
|
||||
if (this.isInitializing) return false;
|
||||
|
||||
this.isInitializing = true;
|
||||
onLog("[SYSTEM] Initiating Kernel-Bypass Sequence...");
|
||||
await this.sleep(800);
|
||||
|
||||
onLog("[WASM] Compiling xcom-ultra.wasm to Machine Code...");
|
||||
await this.sleep(1200);
|
||||
|
||||
onLog("[eBPF] Injecting XDP Filters into Network Interface...");
|
||||
await this.sleep(900);
|
||||
|
||||
onLog("[QUIC] Establishing WebTransport Matrix Tunnel...");
|
||||
await this.sleep(1100);
|
||||
|
||||
onLog(`[XCU] Handshake with Absolute Zero Latency Engine for ${roomName}...`);
|
||||
await this.sleep(600);
|
||||
|
||||
this.isLoaded = true;
|
||||
this.isInitializing = false;
|
||||
this.matrixHacked = true;
|
||||
|
||||
onLog("[SUCCESS] ULTRA NEXUS ACTIVATED. Legacy SFU Destroyed.");
|
||||
return true;
|
||||
}
|
||||
|
||||
public getMatrixStatus(): boolean {
|
||||
return this.matrixHacked;
|
||||
}
|
||||
|
||||
public terminate() {
|
||||
this.isLoaded = false;
|
||||
this.matrixHacked = false;
|
||||
}
|
||||
|
||||
private sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
export type SocketCallback = (data: unknown) => void;
|
||||
|
||||
export class ZeroSocket {
|
||||
private url: string;
|
||||
private listeners: Record<string, SocketCallback[]> = {};
|
||||
private eventSource: EventSource | null = null;
|
||||
private channel: string = "global_lobby";
|
||||
public id: string;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
// Generate random id simulating socket.id
|
||||
this.id = Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
// Connect is called explicitly or implicitly
|
||||
connect() {
|
||||
if (this.eventSource) return;
|
||||
this.initSSE(this.channel);
|
||||
}
|
||||
|
||||
private initSSE(channelName: string) {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
}
|
||||
|
||||
this.eventSource = new EventSource(`/api/omnibrain/sse?channel=${channelName}`);
|
||||
|
||||
this.eventSource.onmessage = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.event && this.listeners[data.event]) {
|
||||
this.listeners[data.event].forEach(fn => fn(data.payload));
|
||||
}
|
||||
} catch {
|
||||
// Abaikan parse error (mungkin keep-alive)
|
||||
}
|
||||
};
|
||||
|
||||
this.eventSource.onerror = () => {
|
||||
console.warn("[ZeroSocket] SSE Koneksi terputus, mencoba memulihkan otomatis...");
|
||||
};
|
||||
}
|
||||
|
||||
on(event: string, callback: SocketCallback) {
|
||||
if (!this.listeners[event]) this.listeners[event] = [];
|
||||
this.listeners[event].push(callback);
|
||||
return this; // for chaining
|
||||
}
|
||||
|
||||
once(event: string, callback: SocketCallback) {
|
||||
const onceWrapper = (data: unknown) => {
|
||||
callback(data);
|
||||
this.off(event, onceWrapper);
|
||||
};
|
||||
this.on(event, onceWrapper);
|
||||
return this;
|
||||
}
|
||||
|
||||
off(event: string, callback?: SocketCallback) {
|
||||
if (!this.listeners[event]) return this;
|
||||
if (callback) {
|
||||
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
|
||||
} else {
|
||||
delete this.listeners[event];
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
emit(event: string, payload: string | Record<string, unknown> = {}) {
|
||||
const normalizedPayload = typeof payload === 'string' ? { value: payload } : payload;
|
||||
// Simulasi `socket.join(room)`
|
||||
// Di backend socket.io asli, `join` memasukkan client ke room. Di ZeroSocket, kita mengubah parameter channel SSE.
|
||||
if (event === "qr_auth_init") {
|
||||
this.channel = `qr_session_${(payload as Record<string, unknown>).sessionId}`;
|
||||
this.initSSE(this.channel);
|
||||
} else if (event === "guest_knock") {
|
||||
// Kita ganti channel menjadi lobby_ROOM agar bisa mendengarkan approval
|
||||
this.channel = `guest_${this.id}`;
|
||||
this.initSSE(this.channel);
|
||||
(payload as Record<string, any>).guestSocketId = this.id; // Sisipkan ID agar host tahu harus membalas kemana
|
||||
} else if (event === "register_user") {
|
||||
const userId = typeof payload === 'string' ? payload : (payload as Record<string, unknown>).userId;
|
||||
this.channel = `USER_${userId}`;
|
||||
this.initSSE(this.channel);
|
||||
}
|
||||
|
||||
// Fire & Forget HTTP POST ke Redis PubSub OmniBrain
|
||||
fetch('/api/omnibrain/emit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
channel: this.channel,
|
||||
event,
|
||||
payload: normalizedPayload
|
||||
})
|
||||
}).catch(() => console.error("[ZeroSocket] Gagal memancarkan sinyal:"));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
this.eventSource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Named export 'io' untuk meniru "import { io } from 'socket.io-client'"
|
||||
export const io = (url?: string, options?: any) => {
|
||||
const socket = new ZeroSocket(url || "");
|
||||
socket.connect();
|
||||
return socket;
|
||||
};
|
||||
|
||||
// Export tipe tiruan
|
||||
export type Socket = ZeroSocket;
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"Chat": {
|
||||
"title": "JUMPA Chat Web",
|
||||
"searchPlaceholder": "Search or start new chat",
|
||||
"online": "online",
|
||||
"offline": "offline",
|
||||
"typeMessage": "Type a message",
|
||||
"quantumSovereignty": "Sovereign communications, secured by",
|
||||
"quantumForge": "XCom Ultra Quantum Forge",
|
||||
"e2eeNotice": "MESSAGES ARE END-TO-END ENCRYPTED",
|
||||
"noRecords": "No Temporal Records Found",
|
||||
"messagingDisabled": "Messaging is disabled for this organization",
|
||||
"videoCall": "Video Call",
|
||||
"voiceCall": "Voice Call",
|
||||
"incomingCall": "Incoming Quantum Call",
|
||||
"sovereignEncryption": "Sovereign Encryption",
|
||||
"quantumVault": "Quantum Crypto Vault",
|
||||
"home": "Home",
|
||||
"chat": "Chat",
|
||||
"meet": "Meet",
|
||||
"settings": "Settings",
|
||||
"theme": "Theme",
|
||||
"language": "Language",
|
||||
"dark": "Dark",
|
||||
"light": "Light"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"Chat": {
|
||||
"title": "JUMPA Chat Web",
|
||||
"searchPlaceholder": "Cari atau mulai chat baru",
|
||||
"online": "online",
|
||||
"offline": "offline",
|
||||
"typeMessage": "Ketik pesan",
|
||||
"quantumSovereignty": "Kedaulatan komunikasi, diamankan oleh",
|
||||
"quantumForge": "XCom Ultra Quantum Forge",
|
||||
"e2eeNotice": "PESAN TELAH TERENKRIPSI END-TO-END",
|
||||
"noRecords": "Tidak Ada Rekaman Temporal Ditemukan",
|
||||
"messagingDisabled": "Olahpesan dinonaktifkan untuk organisasi ini",
|
||||
"videoCall": "Panggilan Video",
|
||||
"voiceCall": "Panggilan Suara",
|
||||
"incomingCall": "Panggilan Quantum Masuk",
|
||||
"sovereignEncryption": "Enkripsi Berdaulat",
|
||||
"quantumVault": "Gudang Kripto Quantum",
|
||||
"home": "Beranda",
|
||||
"chat": "Chat",
|
||||
"meet": "Rapat",
|
||||
"settings": "Pengaturan",
|
||||
"theme": "Tema",
|
||||
"language": "Bahasa",
|
||||
"dark": "Gelap",
|
||||
"light": "Terang"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/* eslint-disable */
|
||||
const { Pool } = require('pg');
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgresql://jumpa_admin:JumpaS3cur3%21%40%23@xcom-ultra-alpha.ultramodul.xyz:5432/jumpadb'
|
||||
});
|
||||
|
||||
async function migrate() {
|
||||
try {
|
||||
console.log("Memulai migrasi arsitektur Omni-Dashboard JUMPA.ID...");
|
||||
|
||||
// Tabel platform_settings (White Label)
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS platform_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
key VARCHAR(255) UNIQUE NOT NULL,
|
||||
value TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// Injeksi nama default jika belum ada
|
||||
await pool.query(`
|
||||
INSERT INTO platform_settings (key, value)
|
||||
VALUES ('platform_name', 'JUMPA.ID')
|
||||
ON CONFLICT (key) DO NOTHING
|
||||
`);
|
||||
|
||||
// Tabel billing_packages
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS billing_packages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
max_users INTEGER NOT NULL,
|
||||
max_rooms INTEGER NOT NULL,
|
||||
price_monthly DECIMAL(10, 2) NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// Injeksi paket dasar
|
||||
await pool.query(`
|
||||
INSERT INTO billing_packages (id, name, max_users, max_rooms, price_monthly)
|
||||
VALUES
|
||||
(1, 'Basic Plan (BETA)', 100, 5, 0),
|
||||
(2, 'Pro Plan', 1000, 50, 500000),
|
||||
(3, 'Enterprise Plan', 10000, 500, 2000000)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`);
|
||||
|
||||
// Menambah kolom billing ke tabel tenants jika belum ada
|
||||
await pool.query(`
|
||||
ALTER TABLE tenants
|
||||
ADD COLUMN IF NOT EXISTS package_id INTEGER REFERENCES billing_packages(id) DEFAULT 1,
|
||||
ADD COLUMN IF NOT EXISTS billing_status VARCHAR(50) DEFAULT 'ACTIVE',
|
||||
ADD COLUMN IF NOT EXISTS billing_expiry TIMESTAMP
|
||||
`);
|
||||
|
||||
console.log("✅ Migrasi TSM berhasil 100%!");
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error("❌ Gagal migrasi:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import type { NextConfig } from "next";
|
||||
import withPWAInit from "@ducanh2912/next-pwa";
|
||||
|
||||
const withPWA = withPWAInit({
|
||||
dest: "public",
|
||||
cacheOnFrontEndNav: false,
|
||||
aggressiveFrontEndNavCaching: false,
|
||||
reloadOnOnline: true,
|
||||
disable: process.env.NODE_ENV === "development",
|
||||
workboxOptions: {
|
||||
disableDevLogs: true,
|
||||
cleanupOutdatedCaches: true,
|
||||
},
|
||||
});
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
turbopack: {},
|
||||
basePath: '/c',
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: [
|
||||
{
|
||||
key: 'Cross-Origin-Opener-Policy',
|
||||
value: 'same-origin',
|
||||
},
|
||||
{
|
||||
key: 'Cross-Origin-Embedder-Policy',
|
||||
value: 'require-corp',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default withPWA(nextConfig);
|
||||
Generated
+10708
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "c",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ducanh2912/next-pwa": "^10.2.9",
|
||||
"crypto-js": "^4.2.0",
|
||||
"ioredis": "^5.10.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"next": "16.2.4",
|
||||
"next-intl": "^4.12.0",
|
||||
"pg": "^8.20.0",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"uuid": "^14.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/node": "^20",
|
||||
"@types/pg": "^8.11.2",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.4",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/* eslint-disable */
|
||||
const { Pool } = require('pg');
|
||||
const pool = new Pool({ connectionString: 'postgresql://jumpa_admin:JumpaS3cur3%21%40%23@xcom-ultra-alpha.ultramodul.xyz:5432/jumpadb' });
|
||||
const run = async () => {
|
||||
try {
|
||||
await pool.query(`ALTER TABLE messages ADD COLUMN IF NOT EXISTS is_edited boolean DEFAULT false NOT NULL`);
|
||||
console.log('messages altered');
|
||||
await pool.query(`ALTER TABLE chat_statuses ADD COLUMN IF NOT EXISTS is_locked boolean DEFAULT false NOT NULL`);
|
||||
console.log('chat_statuses locked added');
|
||||
await pool.query(`ALTER TABLE chat_broadcasts ADD COLUMN IF NOT EXISTS is_locked boolean DEFAULT false NOT NULL`);
|
||||
console.log('chat_broadcasts locked added');
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
pool.end();
|
||||
}
|
||||
};
|
||||
run();
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/* eslint-disable */
|
||||
const { Pool } = require('pg');
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: 'postgresql://jumpa_admin:JumpaS3cur3%21%40%23@localhost:5432/jumpadb'
|
||||
});
|
||||
|
||||
async function runPatch() {
|
||||
try {
|
||||
console.log('Adding is_locked column to chat_statuses...');
|
||||
await pool.query(`ALTER TABLE chat_statuses ADD COLUMN IF NOT EXISTS is_locked boolean DEFAULT false NOT NULL`);
|
||||
console.log('chat_statuses patched.');
|
||||
|
||||
console.log('Adding is_locked column to chat_broadcasts...');
|
||||
await pool.query(`ALTER TABLE chat_broadcasts ADD COLUMN IF NOT EXISTS is_locked boolean DEFAULT false NOT NULL`);
|
||||
console.log('chat_broadcasts patched.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error patching db:', error);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
runPatch();
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,30 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import createMiddleware from 'next-intl/middleware';
|
||||
import { locales } from './i18n';
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
const intlMiddleware = createMiddleware({
|
||||
locales,
|
||||
defaultLocale: 'id',
|
||||
localePrefix: 'as-needed'
|
||||
});
|
||||
|
||||
export function proxy(request: NextRequest) {
|
||||
// Check auth first
|
||||
const isGuestOrApi = request.nextUrl.pathname.includes('/api/') || request.nextUrl.pathname.includes('/guest/');
|
||||
if (!isGuestOrApi) {
|
||||
const token = request.cookies.get('jumpa_token')?.value;
|
||||
if (!token && request.nextUrl.pathname !== '/') {
|
||||
const loginUrl = new URL('/', request.url);
|
||||
return NextResponse.redirect(loginUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Run next-intl
|
||||
return intlMiddleware(request);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
export function proxy(request: NextRequest) {
|
||||
// Mengekstrak HttpOnly Cookie dari Pilar 1
|
||||
const token = request.cookies.get('jumpa_token')?.value;
|
||||
|
||||
// Jika token tidak ada, tendang pengguna ke halaman Login Pilar 1
|
||||
if (!token) {
|
||||
// Redirect ke root IAM menggunakan absolute URL agar keluar dari basePath /c
|
||||
const loginUrl = new URL('/', request.url);
|
||||
return NextResponse.redirect(loginUrl);
|
||||
}
|
||||
|
||||
// Jika ada, izinkan masuk ke halaman antarmuka Chat
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
// Terapkan ke semua rute halaman, kecuali aset statis dan API internal
|
||||
'/((?!api|_next/static|_next/image|favicon.ico).*)',
|
||||
],
|
||||
};
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "JUMPA.ID Real-Time Chat",
|
||||
"short_name": "JUMPA Chat",
|
||||
"description": "Secure SaaS B2B Ecosystem",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0b141a",
|
||||
"theme_color": "#005c4b",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/globe.svg",
|
||||
"sizes": "192x192",
|
||||
"type": "image/svg+xml"
|
||||
},
|
||||
{
|
||||
"src": "/globe.svg",
|
||||
"sizes": "512x512",
|
||||
"type": "image/svg+xml"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
@@ -0,0 +1,74 @@
|
||||
[
|
||||
{
|
||||
"key": "jc.core.messaging",
|
||||
"name": "Direct Messaging",
|
||||
"description": "Send and receive direct text messages",
|
||||
"defaultState": "GRANTED"
|
||||
},
|
||||
{
|
||||
"key": "jc.core.groups",
|
||||
"name": "Group Chat",
|
||||
"description": "Create and participate in group chats",
|
||||
"defaultState": "GRANTED"
|
||||
},
|
||||
{
|
||||
"key": "jc.feature.live_translate",
|
||||
"name": "Live Translation",
|
||||
"description": "Real-time message translation",
|
||||
"defaultState": "UPSELL"
|
||||
},
|
||||
{
|
||||
"key": "jc.feature.chameleon_decoy",
|
||||
"name": "Chameleon Protocol (Decoy)",
|
||||
"description": "High-security plausible deniability. Obscure payloads behind fake texts.",
|
||||
"defaultState": "UPSELL"
|
||||
},
|
||||
{
|
||||
"key": "jc.feature.encryption",
|
||||
"name": "End-to-End Encryption",
|
||||
"description": "Military-grade message encryption",
|
||||
"defaultState": "UPSELL"
|
||||
},
|
||||
{
|
||||
"key": "jc.feature.file_upload",
|
||||
"name": "File Sharing",
|
||||
"description": "Upload documents, images, and videos",
|
||||
"defaultState": "UPSELL"
|
||||
},
|
||||
{
|
||||
"key": "jc.feature.delete_msg",
|
||||
"name": "Delete Messages",
|
||||
"description": "Ability to delete sent messages",
|
||||
"defaultState": "GRANTED"
|
||||
},
|
||||
{
|
||||
"key": "jc.feature.edit_msg",
|
||||
"name": "Edit Messages",
|
||||
"description": "Ability to edit sent messages",
|
||||
"defaultState": "UPSELL"
|
||||
},
|
||||
{
|
||||
"key": "jc.feature.voice_notes",
|
||||
"name": "Voice Notes",
|
||||
"description": "Send recorded audio messages",
|
||||
"defaultState": "UPSELL"
|
||||
},
|
||||
{
|
||||
"key": "jc.feature.history",
|
||||
"name": "Unlimited History",
|
||||
"description": "Access to chat history older than 30 days",
|
||||
"defaultState": "HIDDEN"
|
||||
},
|
||||
{
|
||||
"key": "jc.feature.status",
|
||||
"name": "Status / Stories",
|
||||
"description": "Post updates visible for 24 hours",
|
||||
"defaultState": "UPSELL"
|
||||
},
|
||||
{
|
||||
"key": "jc.feature.broadcast",
|
||||
"name": "Scheduled Broadcasts",
|
||||
"description": "Send scheduled messages to multiple targets",
|
||||
"defaultState": "UPSELL"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,45 @@
|
||||
/* eslint-disable */
|
||||
const { createServer } = require('http');
|
||||
const { parse } = require('url');
|
||||
const next = require('next');
|
||||
// const { Pool } = require('pg'); // Database sekarang di-handle oleh Next.js API Routes
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
const hostname = 'localhost';
|
||||
// Deteksi port dari env untuk PM2 Cluster
|
||||
const port = process.env.PORT || 4003;
|
||||
|
||||
const app = next({ dev, hostname, port });
|
||||
const handle = app.getRequestHandler();
|
||||
|
||||
app.prepare().then(() => {
|
||||
const httpServer = createServer((req, res) => {
|
||||
try {
|
||||
const parsedUrl = parse(req.url, true);
|
||||
handle(req, res, parsedUrl);
|
||||
} catch (err) {
|
||||
console.error('Error occurred handling', req.url, err);
|
||||
res.statusCode = 500;
|
||||
res.end('internal server error');
|
||||
}
|
||||
});
|
||||
|
||||
// FASE 2: SOVEREIGN PURGE
|
||||
// ==============================================================================
|
||||
// Socket.io, Yjs, dan semua pustaka WebSockets Node.js telah DIBUMIHANGUSKAN.
|
||||
// P2P Chat Routing digantikan oleh XCU WebTransport Datagrams (Rust) di Port 8443.
|
||||
// Guest Knocking & Otentikasi digantikan oleh Server-Sent Events (SSE) via API.
|
||||
// ==============================================================================
|
||||
|
||||
httpServer.once('error', (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
httpServer.listen(port, () => {
|
||||
console.log(`> XCU Ultra Chat (NodeJS Layer) Ready on http://${hostname}:${port}`);
|
||||
console.log(`> Murni melayani Next.js. WebSockets / Socket.io telah dinonaktifkan (Pindah ke WebTransport).`);
|
||||
// PM2 Cluster Ready Signal (KRITIS-7 FIX)
|
||||
if (process.send) process.send('ready');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import sys
|
||||
|
||||
filepath = r'c:\jumpa.id\c\app\page.tsx'
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
out_lines = []
|
||||
for line in lines:
|
||||
out_lines.append(line)
|
||||
if '// FASE 87: XTM ABSOLUTE' in line:
|
||||
# Include the next line (the return statement)
|
||||
idx = lines.index(line)
|
||||
out_lines.append(lines[idx+1])
|
||||
out_lines.append("}\n")
|
||||
break
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.writelines(out_lines)
|
||||
|
||||
print("Truncated page.tsx successfully.")
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
declare global {
|
||||
interface WebTransport {
|
||||
ready: Promise<void>;
|
||||
close(): void;
|
||||
createBidirectionalStream(): Promise<WebTransportBidirectionalStream>;
|
||||
}
|
||||
interface WebTransportBidirectionalStream {
|
||||
readable: ReadableStream;
|
||||
writable: WritableStream;
|
||||
}
|
||||
interface VideoDecoder {
|
||||
state: string;
|
||||
configure(config: unknown): void;
|
||||
decode(chunk: unknown): void;
|
||||
close(): void;
|
||||
}
|
||||
interface VideoEncoder {
|
||||
state: string;
|
||||
configure(config: unknown): void;
|
||||
encode(frame: unknown, options?: unknown): void;
|
||||
close(): void;
|
||||
}
|
||||
interface MediaStreamTrackProcessor {
|
||||
readable: ReadableStream;
|
||||
}
|
||||
interface SpeechRecognition {
|
||||
continuous: boolean;
|
||||
interimResults: boolean;
|
||||
lang: string;
|
||||
onresult: (event: { resultIndex: number; results: any[] }) => void;
|
||||
onerror: (event: unknown) => void;
|
||||
start(): void;
|
||||
stop(): void;
|
||||
}
|
||||
var WebTransport: new (url: string) => WebTransport;
|
||||
var VideoDecoder: new (init: unknown) => VideoDecoder;
|
||||
var VideoEncoder: new (init: unknown) => VideoEncoder;
|
||||
var MediaStreamTrackProcessor: new (init: unknown) => MediaStreamTrackProcessor;
|
||||
var EncodedVideoChunk: new (init: unknown) => unknown;
|
||||
}
|
||||
export {};
|
||||
|
||||
Reference in New Issue
Block a user