From 49fa5aa2a127bdf8924d02bf77e5086b39c7a447 Mon Sep 17 00:00:00 2001 From: Calvin Morrison Date: Wed, 3 Sep 2025 21:15:36 -0400 Subject: i vibe coded it --- client/jmap-client.js | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 client/jmap-client.js (limited to 'client/jmap-client.js') diff --git a/client/jmap-client.js b/client/jmap-client.js new file mode 100644 index 0000000..a7d5a37 --- /dev/null +++ b/client/jmap-client.js @@ -0,0 +1,302 @@ +/** + * 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; + } +} -- cgit v1.2.3