import "dotenv/config"; import { SignalClient } from "./signal-crypto.js"; import { v4 as uuidv4 } from "uuid"; export class EncryptedTodoService { constructor() { // In-memory storage using Maps this.users = new Map(); // userId -> SignalClient this.userIds = new Set(); // Set of all user IDs this.todos = new Map(); // todoId -> todo object this.todoGroups = new Map(); // todoGroupId -> group info this.todoParticipants = new Map(); // todoGroupId -> Set of userIds this.userTodos = new Map(); // userId -> Set of todoGroupIds console.log("Initialized in-memory encrypted todo service"); } async createUser(userId) { this.userIds.add(userId); if (!this.users.has(userId)) { this.users.set(userId, SignalClient.create(userId)); this.userTodos.set(userId, new Set()); } return { userId }; } async getAllUsers() { return Array.from(this.userIds); } async addTodo(userId, todoText) { const todoGroupId = uuidv4(); const client = this.users.get(userId); const todoId = uuidv4(); const encryptedTodo = client.encrypt(todoText); const now = new Date().toISOString(); // Create the todo group this.todoGroups.set(todoGroupId, { todoGroupId, createdBy: userId, createdAt: now, }); // Add the creator as a participant this.todoParticipants.set(todoGroupId, new Set([userId])); // Create the todo entry for the creator const todoKey = `${todoGroupId}_${userId}`; this.todos.set(todoKey, { id: todoId, todoGroupId, userId, encrypted: encryptedTodo, createdAt: now, originalText: todoText, completed: false, }); // Add to user's todo list this.userTodos.get(userId).add(todoGroupId); return todoGroupId; } async getTodos(userId) { const client = this.users.get(userId); if (!client) { console.error(`No client found for user ${userId}`); return []; } const userTodoGroups = this.userTodos.get(userId) || new Set(); const todos = []; for (const todoGroupId of userTodoGroups) { try { const todoKey = `${todoGroupId}_${userId}`; const todoData = this.todos.get(todoKey); if (!todoData) { console.warn(`Todo data not found for ${todoKey}`); continue; } const decryptedText = client.decrypt(todoData.encrypted); const groupInfo = this.todoGroups.get(todoGroupId); const participants = Array.from( this.todoParticipants.get(todoGroupId) || new Set(), ); todos.push({ id: todoGroupId, // Use todoGroupId as the primary identifier text: decryptedText, createdAt: todoData.createdAt, completed: todoData.completed, participants, createdBy: groupInfo?.createdBy || userId, isCreator: (groupInfo?.createdBy || userId) === userId, }); } catch (e) { console.error(`Error processing todo ${todoGroupId}:`, e); } } return todos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); } async getTodo(userId, todoGroupId) { const client = this.users.get(userId); const todoKey = `${todoGroupId}_${userId}`; const todoData = this.todos.get(todoKey); if (!todoData) throw new Error("Todo not found"); const decryptedText = client.decrypt(todoData.encrypted); const groupInfo = this.todoGroups.get(todoGroupId); const participants = Array.from( this.todoParticipants.get(todoGroupId) || new Set(), ); return { id: todoGroupId, text: decryptedText, createdAt: todoData.createdAt, completed: todoData.completed, participants, createdBy: groupInfo?.createdBy, isCreator: groupInfo?.createdBy === userId, }; } async updateTodoStatus(userId, todoGroupId, completed) { console.log( `updateTodoStatus called: userId=${userId}, todoGroupId=${todoGroupId}, completed=${completed}`, ); const participants = this.todoParticipants.get(todoGroupId); if (!participants || !participants.has(userId)) { throw new Error("User is not a participant in this todo group"); } const participantIds = Array.from(participants); console.log(`Updating todo for all participants:`, participantIds); // Update todos for all participants for (const participantId of participantIds) { const todoKey = `${todoGroupId}_${participantId}`; const todoData = this.todos.get(todoKey); if (todoData) { todoData.completed = completed; this.todos.set(todoKey, todoData); } } console.log(`Affected users for todo update:`, participantIds); return participantIds; } async deleteTodo(userId, todoGroupId) { const participants = this.todoParticipants.get(todoGroupId); if (!participants || !participants.has(userId)) { throw new Error("User is not a participant in this todo group"); } const groupInfo = this.todoGroups.get(todoGroupId); if (!groupInfo) { throw new Error("Todo group not found"); } if (groupInfo.createdBy !== userId) { throw new Error("Only the creator can delete this todo"); } const participantIds = Array.from(participants); console.log( `Deleting todo group ${todoGroupId} for all participants:`, participantIds, ); // Delete all todo entries for this group for (const participantId of participantIds) { const todoKey = `${todoGroupId}_${participantId}`; this.todos.delete(todoKey); // Remove from user's todo list const userTodoSet = this.userTodos.get(participantId); if (userTodoSet) { userTodoSet.delete(todoGroupId); } } // Delete participants and group this.todoParticipants.delete(todoGroupId); this.todoGroups.delete(todoGroupId); return { deletedIds: [todoGroupId], affectedUsers: participantIds, }; } async shareTodo(userId, todoGroupId, recipientId) { // Ensure recipient user exists if (!this.users.has(recipientId)) { this.users.set(recipientId, SignalClient.create(recipientId)); this.userTodos.set(recipientId, new Set()); this.userIds.add(recipientId); } const participants = this.todoParticipants.get(todoGroupId); if (!participants || !participants.has(userId)) { throw new Error("User is not a participant in this todo group"); } const groupInfo = this.todoGroups.get(todoGroupId); if (!groupInfo) { throw new Error("Todo group not found"); } if (groupInfo.createdBy !== userId) { throw new Error("Only the creator can add participants to this todo"); } if (participants.has(recipientId)) { throw new Error("User is already a participant in this todo group"); } // Get the original text from the sender's todo const senderTodoKey = `${todoGroupId}_${userId}`; const senderTodoData = this.todos.get(senderTodoKey); if (!senderTodoData) throw new Error("Todo not found"); const recipientClient = this.users.get(recipientId); const encryptedForRecipient = recipientClient.encrypt( senderTodoData.originalText, ); const recipientTodoId = uuidv4(); const now = new Date().toISOString(); // Add recipient as a participant participants.add(recipientId); this.todoParticipants.set(todoGroupId, participants); // Create todo entry for the recipient const recipientTodoKey = `${todoGroupId}_${recipientId}`; this.todos.set(recipientTodoKey, { id: recipientTodoId, todoGroupId, userId: recipientId, encrypted: encryptedForRecipient, createdAt: now, originalText: senderTodoData.originalText, completed: senderTodoData.completed, }); // Add to recipient's todo list this.userTodos.get(recipientId).add(todoGroupId); return todoGroupId; } }