Files
kashilo/js/services/chat.js

218 lines
6.5 KiB
JavaScript

/**
* Chat Service - Handles message storage and retrieval
* Uses LocalStorage as mock backend until Directus is ready
*/
import { cryptoService } from './crypto.js'
const CHATS_STORAGE_KEY = 'dgray_chats'
const MESSAGES_STORAGE_KEY = 'dgray_messages'
class ChatService {
constructor() {
this.subscribers = new Set()
}
/**
* Get or create a chat between current user and another user
* @param {string} recipientId - The other user's ID
* @param {string} recipientPublicKey - The other user's public key
* @param {string} listingId - The listing this chat is about
* @returns {object} - Chat object
*/
getOrCreateChat(recipientId, recipientPublicKey, listingId) {
const chats = this.getAllChats()
const myPublicKey = cryptoService.getPublicKey()
// Find existing chat for this listing + recipient
let chat = chats.find(c =>
c.listingId === listingId &&
c.recipientId === recipientId
)
if (!chat) {
chat = {
id: this.generateId(),
listingId,
recipientId,
recipientPublicKey,
myPublicKey,
createdAt: new Date().toISOString(),
lastMessageAt: null
}
chats.push(chat)
this.saveChats(chats)
}
return chat
}
getAllChats() {
try {
return JSON.parse(localStorage.getItem(CHATS_STORAGE_KEY) || '[]')
} catch {
return []
}
}
saveChats(chats) {
localStorage.setItem(CHATS_STORAGE_KEY, JSON.stringify(chats))
}
/**
* Send an encrypted message
* @param {string} chatId - Chat ID
* @param {string} recipientPublicKey - Recipient's public key
* @param {string} plainText - Message content
* @returns {object} - The saved message
*/
async sendMessage(chatId, recipientPublicKey, plainText) {
await cryptoService.ready
const { nonce, ciphertext } = cryptoService.encrypt(plainText, recipientPublicKey)
const message = {
id: this.generateId(),
chatId,
senderPublicKey: cryptoService.getPublicKey(),
nonce,
ciphertext,
timestamp: new Date().toISOString(),
// Store plain text for own messages (we can't decrypt our own box messages)
_plainText: plainText
}
const messages = this.getAllMessages()
messages.push(message)
this.saveMessages(messages)
// Update chat's lastMessageAt
const chats = this.getAllChats()
const chat = chats.find(c => c.id === chatId)
if (chat) {
chat.lastMessageAt = message.timestamp
this.saveChats(chats)
}
this.notifySubscribers()
return message
}
/**
* Get all messages for a chat, decrypted
* @param {string} chatId - Chat ID
* @param {string} otherPublicKey - The other party's public key
* @returns {Array} - Decrypted messages
*/
async getMessages(chatId, otherPublicKey) {
await cryptoService.ready
const messages = this.getAllMessages().filter(m => m.chatId === chatId)
const myPublicKey = cryptoService.getPublicKey()
return messages.map(msg => {
const isOwn = msg.senderPublicKey === myPublicKey
let text
if (isOwn) {
// Use stored plain text for own messages
text = msg._plainText || '[Encrypted]'
} else {
// Decrypt messages from others
text = cryptoService.decrypt(msg.ciphertext, msg.nonce, msg.senderPublicKey)
if (!text) text = '[Decryption failed]'
}
return {
id: msg.id,
text,
isOwn,
timestamp: msg.timestamp
}
})
}
getAllMessages() {
try {
return JSON.parse(localStorage.getItem(MESSAGES_STORAGE_KEY) || '[]')
} catch {
return []
}
}
saveMessages(messages) {
localStorage.setItem(MESSAGES_STORAGE_KEY, JSON.stringify(messages))
}
generateId() {
return 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
}
subscribe(callback) {
this.subscribers.add(callback)
return () => this.subscribers.delete(callback)
}
notifySubscribers() {
this.subscribers.forEach(cb => cb())
}
/**
* Simulate receiving a message (for demo purposes)
* In production, this would come from Directus/WebSocket
*/
async simulateIncomingMessage(chatId, senderPublicKey, plainText) {
await cryptoService.ready
const myPublicKey = cryptoService.getPublicKey()
const { nonce, ciphertext } = this.encryptForRecipient(plainText, myPublicKey, senderPublicKey)
const message = {
id: this.generateId(),
chatId,
senderPublicKey,
nonce,
ciphertext,
timestamp: new Date().toISOString()
}
const messages = this.getAllMessages()
messages.push(message)
this.saveMessages(messages)
this.notifySubscribers()
return message
}
// Helper for simulation - encrypt as if from another user
encryptForRecipient(message, recipientPublicKey, senderSecretKey) {
// This is a simplified simulation - in reality the sender would have their own keypair
const nacl = window.nacl
const naclUtil = window.nacl.util
const nonce = nacl.randomBytes(nacl.box.nonceLength)
const messageUint8 = naclUtil.decodeUTF8(message)
// For simulation, we create a temporary keypair for the "sender"
const senderKeyPair = nacl.box.keyPair.fromSecretKey(
naclUtil.decodeBase64(senderSecretKey || naclUtil.encodeBase64(nacl.randomBytes(32)))
)
const encrypted = nacl.box(
messageUint8,
nonce,
naclUtil.decodeBase64(recipientPublicKey),
senderKeyPair.secretKey
)
return {
nonce: naclUtil.encodeBase64(nonce),
ciphertext: naclUtil.encodeBase64(encrypted)
}
}
}
export const chatService = new ChatService()