aboutsummaryrefslogtreecommitdiff
path: root/client/jmap-client.js
diff options
context:
space:
mode:
Diffstat (limited to 'client/jmap-client.js')
-rw-r--r--client/jmap-client.js302
1 files changed, 302 insertions, 0 deletions
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;
+ }
+}