/** * JMAP Client Library for JCHAT * Provides a clean interface for interacting with JMAP-based chat servers */ class JMAPClient { constructor(serverUrl) { this.serverUrl = serverUrl; this.session = null; this.accountId = 'default'; this.capabilities = []; this.authToken = null; } /** * Initialize the JMAP session with optional auth token */ async init(authToken = null) { this.authToken = authToken; const headers = { 'Accept': 'application/json' }; if (this.authToken) { headers['Authorization'] = `Bearer ${this.authToken}`; } const response = await fetch(`${this.serverUrl}/jmap/session`, { method: 'GET', headers: headers }); if (!response.ok) { if (response.status === 401) { throw new Error('Authentication required'); } throw new Error(`Failed to load session: ${response.status} ${response.statusText}`); } this.session = await response.json(); this.accountId = Object.keys(this.session.accounts)[0] || 'default'; this.capabilities = Object.keys(this.session.capabilities); return this.session; } /** * Make a JMAP API request */ async request(methodCalls, using = null) { if (!this.session) { throw new Error('Session not initialized. Call init() first.'); } const defaultUsing = ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:chat']; const requestBody = { using: using || defaultUsing, methodCalls: methodCalls }; const headers = { 'Content-Type': 'application/json; charset=utf-8' }; if (this.authToken) { headers['Authorization'] = `Bearer ${this.authToken}`; } const response = await fetch(`${this.serverUrl}/jmap/api`, { method: 'POST', headers: headers, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`JMAP request failed: ${response.status} ${response.statusText}\n${errorText}`); } const result = await response.json(); if (result.methodResponses) { return result; } else { throw new Error('Invalid JMAP response format'); } } /** * Get conversations */ async getConversations(ids = null, properties = null) { const methodCall = ['Conversation/get', { accountId: this.accountId, ids: ids, properties: properties }, 'c1']; const response = await this.request([methodCall]); const [method, result, callId] = response.methodResponses[0]; if (method === 'error') { throw new Error(`Conversation/get error: ${result.type} - ${result.description || ''}`); } return result; } /** * Create a new conversation */ async createConversation(data) { const methodCall = ['Conversation/set', { accountId: this.accountId, create: { 'new-conv': data } }, 'c1']; const response = await this.request([methodCall]); const [method, result, callId] = response.methodResponses[0]; if (method === 'error') { throw new Error(`Conversation/set error: ${result.type} - ${result.description || ''}`); } if (result.notCreated && result.notCreated['new-conv']) { throw new Error(`Failed to create conversation: ${result.notCreated['new-conv'].type}`); } return result.created['new-conv']; } /** * Update a conversation */ async updateConversation(id, updates) { const methodCall = ['Conversation/set', { accountId: this.accountId, update: { [id]: updates } }, 'c1']; const response = await this.request([methodCall]); const [method, result, callId] = response.methodResponses[0]; if (method === 'error') { throw new Error(`Conversation/set error: ${result.type} - ${result.description || ''}`); } if (result.notUpdated && result.notUpdated[id]) { throw new Error(`Failed to update conversation: ${result.notUpdated[id].type}`); } return result.updated.find(conv => conv.id === id); } /** * Query conversations */ async queryConversations(filter = {}, sort = null) { const methodCall = ['Conversation/query', { accountId: this.accountId, filter: filter, sort: sort }, 'c1']; const response = await this.request([methodCall]); const [method, result, callId] = response.methodResponses[0]; if (method === 'error') { throw new Error(`Conversation/query error: ${result.type} - ${result.description || ''}`); } return result; } /** * Get messages */ async getMessages(ids = null, properties = null) { const methodCall = ['Message/get', { accountId: this.accountId, ids: ids, properties: properties }, 'c1']; const response = await this.request([methodCall]); const [method, result, callId] = response.methodResponses[0]; if (method === 'error') { throw new Error(`Message/get error: ${result.type} - ${result.description || ''}`); } return result; } /** * Send a message */ async sendMessage(conversationId, body, bodyType = 'text/plain', senderId = null) { const methodCall = ['Message/set', { accountId: this.accountId, create: { 'new-message': { conversationId: conversationId, body: body, bodyType: bodyType, senderId: senderId || this.accountId // Use provided senderId or default to accountId } } }, 'c1']; const response = await this.request([methodCall]); const [method, result, callId] = response.methodResponses[0]; if (method === 'error') { throw new Error(`Message/set error: ${result.type} - ${result.description || ''}`); } if (result.notCreated && result.notCreated['new-message']) { throw new Error(`Failed to send message: ${result.notCreated['new-message'].type}`); } return result.created['new-message']; } /** * Query messages */ async queryMessages(filter = {}, sort = null) { const methodCall = ['Message/query', { accountId: this.accountId, filter: filter, sort: sort }, 'c1']; const response = await this.request([methodCall]); const [method, result, callId] = response.methodResponses[0]; if (method === 'error') { throw new Error(`Message/query error: ${result.type} - ${result.description || ''}`); } return result; } /** * Get conversation changes */ async getConversationChanges(sinceState) { const methodCall = ['Conversation/changes', { accountId: this.accountId, sinceState: sinceState }, 'c1']; const response = await this.request([methodCall]); const [method, result, callId] = response.methodResponses[0]; if (method === 'error') { throw new Error(`Conversation/changes error: ${result.type} - ${result.description || ''}`); } return result; } /** * Get message changes */ async getMessageChanges(sinceState) { const methodCall = ['Message/changes', { accountId: this.accountId, sinceState: sinceState }, 'c1']; const response = await this.request([methodCall]); const [method, result, callId] = response.methodResponses[0]; if (method === 'error') { throw new Error(`Message/changes error: ${result.type} - ${result.description || ''}`); } return result; } /** * Echo test method */ async echo(data) { const methodCall = ['Core/echo', data, 'c1']; const response = await this.request([methodCall]); const [method, result, callId] = response.methodResponses[0]; if (method === 'error') { throw new Error(`Core/echo error: ${result.type} - ${result.description || ''}`); } return result; } }