UNPKG

@xmtp/node-sdk

Version:

XMTP Node client SDK for interacting with XMTP networks

1,497 lines (1,481 loc) 71.9 kB
import { GroupUpdatedCodec, ContentTypeGroupUpdated } from '@xmtp/content-type-group-updated'; import { ContentTypeText, TextCodec } from '@xmtp/content-type-text'; import { generateInboxId as generateInboxId$1, getInboxIdForIdentifier as getInboxIdForIdentifier$1, createClient as createClient$1, revokeInstallationsSignatureRequest, applySignatureRequest, inboxStateFromInboxIds, verifySignedWithPublicKey, isAddressAuthorized, isInstallationAuthorized } from '@xmtp/node-bindings'; export { ConsentEntityType, ConsentState, ConversationType, DeliveryStatus, GroupMember, GroupMembershipState, GroupMessageKind, GroupMetadata, GroupPermissions, GroupPermissionsOptions, IdentifierKind, LogLevel, MetadataField, PermissionLevel, PermissionPolicy, PermissionUpdateType, SignatureRequestHandle, SortDirection } from '@xmtp/node-bindings'; import { ContentTypeId } from '@xmtp/content-type-primitives'; import { isPromise } from 'node:util/types'; import { join } from 'node:path'; import process from 'node:process'; import bindingsVersion from '@xmtp/node-bindings/version.json' with { type: 'json' }; /** * Pre-configured URLs for the XMTP network based on the environment * * @constant * @property {string} local - The local URL for the XMTP network * @property {string} dev - The development URL for the XMTP network * @property {string} production - The production URL for the XMTP network */ const ApiUrls = { local: "http://localhost:5556", dev: "https://grpc.dev.xmtp.network:443", production: "https://grpc.production.xmtp.network:443", }; /** * Pre-configured URLs for the XMTP history sync service based on the environment * * @constant * @property {string} local - The local URL for the XMTP history sync service * @property {string} dev - The development URL for the XMTP history sync service * @property {string} production - The production URL for the XMTP history sync service */ const HistorySyncUrls = { local: "http://localhost:5558", dev: "https://message-history.dev.ephemera.network", production: "https://message-history.production.ephemera.network", }; function nsToDate(ns) { return new Date(ns / 1_000_000); } /** * Represents a decoded XMTP message * * This class transforms network messages into a structured format with * content decoding. * * @class * @property {any} content - The decoded content of the message * @property {ContentTypeId} contentType - The content type of the message content * @property {string} conversationId - Unique identifier for the conversation * @property {MessageDeliveryStatus} deliveryStatus - Current delivery status of the message ("unpublished" | "published" | "failed") * @property {string} [fallback] - Optional fallback text for the message * @property {number} [compression] - Optional compression level applied to the message * @property {string} id - Unique identifier for the message * @property {MessageKind} kind - Type of message ("application" | "membership_change") * @property {Record<string, string>} parameters - Additional parameters associated with the message * @property {string} senderInboxId - Identifier for the sender's inbox * @property {Date} sentAt - Timestamp when the message was sent * @property {number} sentAtNs - Timestamp when the message was sent (in nanoseconds) */ class DecodedMessage { #client; content; contentType; conversationId; deliveryStatus; fallback; compression; id; kind; parameters; senderInboxId; sentAt; sentAtNs; constructor(client, message) { this.#client = client; this.id = message.id; this.sentAtNs = message.sentAtNs; this.sentAt = nsToDate(message.sentAtNs); this.conversationId = message.convoId; this.senderInboxId = message.senderInboxId; switch (message.kind) { case 0 /* GroupMessageKind.Application */: this.kind = "application"; break; case 1 /* GroupMessageKind.MembershipChange */: this.kind = "membership_change"; break; // no default } switch (message.deliveryStatus) { case 0 /* DeliveryStatus.Unpublished */: this.deliveryStatus = "unpublished"; break; case 1 /* DeliveryStatus.Published */: this.deliveryStatus = "published"; break; case 2 /* DeliveryStatus.Failed */: this.deliveryStatus = "failed"; break; // no default } this.contentType = message.content.type ? new ContentTypeId(message.content.type) : undefined; this.parameters = message.content.parameters; this.fallback = message.content.fallback; this.compression = message.content.compression; this.content = undefined; if (this.contentType) { try { this.content = this.#client.decodeContent(message, this.contentType); } catch { this.content = undefined; } } } } class CodecNotFoundError extends Error { constructor(contentType) { super(`Codec not found for "${contentType.toString()}" content type`); } } class InboxReassignError extends Error { constructor() { super("Unable to create add account signature text, `allowInboxReassign` must be true"); } } class AccountAlreadyAssociatedError extends Error { constructor(inboxId) { super(`Account already associated with inbox ${inboxId}`); } } class InvalidGroupMembershipChangeError extends Error { constructor(messageId) { super(`Invalid group membership change for message ${messageId}`); } } class MissingContentTypeError extends Error { constructor() { super("Content type is required when sending content other than text"); } } class SignerUnavailableError extends Error { constructor() { super("Signer unavailable, use Client.create to create a client with a signer"); } } class ClientNotInitializedError extends Error { constructor() { super("Client not initialized, use Client.create or Client.build to create a client"); } } class StreamFailedError extends Error { constructor(retryAttempts) { const times = `time${retryAttempts !== 1 ? "s" : ""}`; super(`Stream failed, retried ${retryAttempts} ${times}`); } } class StreamInvalidRetryAttemptsError extends Error { constructor() { super("Stream retry attempts must be greater than 0"); } } /** * AsyncStream provides an async iterable interface for streaming data. * * This class implements a producer-consumer pattern where: * - Producers can push values using the `push()` method * - Consumers can iterate over values asynchronously using `for await` loops or `next()` * - Values are queued internally when no consumers are waiting * - Consumers are resolved immediately when values are available * - The stream can be terminated using `done()`, `return()`, or `end()` * * @example * ```typescript * const stream = new AsyncStream<string>(); * * stream.push("hello"); * stream.push("world"); * * for await (const value of stream) { * console.log(value); // "hello", "world" * } * ``` */ class AsyncStream { isDone = false; #pendingResolves = []; #queue; onDone; onReturn; constructor() { this.#queue = []; this.isDone = false; } flush() { while (this.#pendingResolves.length > 0) { const nextResolve = this.#pendingResolves.shift(); if (nextResolve) { nextResolve({ done: true, value: undefined }); } } } done() { this.flush(); this.#queue = []; this.#pendingResolves = []; this.isDone = true; this.onDone?.(); } push = (value) => { if (this.isDone) { return; } const nextResolve = this.#pendingResolves.shift(); if (nextResolve) { nextResolve({ done: false, value, }); } else { this.#queue.push(value); } }; next = () => { if (this.isDone) { return Promise.resolve({ done: true, value: undefined }); } if (this.#queue.length > 0) { return Promise.resolve({ done: false, value: this.#queue.shift(), }); } return new Promise((resolve) => { this.#pendingResolves.push(resolve); }); }; return = () => { this.onReturn?.(); this.done(); return Promise.resolve({ done: true, value: undefined, }); }; end = () => this.return(); [Symbol.asyncIterator]() { return this; } } const usableProperties = [ "end", "isDone", "next", "return", Symbol.asyncIterator, ]; const isUsableProperty = (prop) => { return usableProperties.includes(prop); }; /** * Creates a read-only proxy for AsyncStream instances that restricts access to consumer-only methods. * * This proxy only exposes the following properties and methods: * - `next()`: Get the next value from the stream * - `end()`: Terminate the stream and stop iteration * - `return()`: Same as end(), terminates the stream * - `isDone`: Boolean indicating if the stream has been terminated * - `Symbol.asyncIterator`: Enables `for await` loop iteration * * Producer methods like `push()`, `done()`, and `flush()` are hidden to prevent * consumers from accidentally modifying the stream state. * * @param stream - The AsyncStream instance to create a proxy for * @returns A read-only proxy that implements AsyncStreamProxy<T> * * @example * ```typescript * const stream = new AsyncStream<string>(); * const proxy = createAsyncStreamProxy(stream); * * stream.push("hello"); * stream.push("world"); * * for await (const value of proxy) { * console.log(value); // "hello", "world" * } * ``` */ function createAsyncStreamProxy(stream) { return new Proxy(stream, { get(target, prop, receiver) { if (isUsableProperty(prop)) { return Reflect.get(target, prop, receiver); } }, set() { return true; }, has(_target, prop) { return isUsableProperty(prop); }, ownKeys() { return usableProperties; }, getOwnPropertyDescriptor(target, prop) { if (isUsableProperty(prop)) { return { enumerable: true, configurable: true, value: Reflect.get(target, prop), }; } return undefined; }, }); } const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const DEFAULT_RETRY_DELAY = 10000; // milliseconds const DEFAULT_RETRY_ATTEMPTS = 6; /** * Creates a stream from a stream function * * If the stream fails, an attempt will be made to restart it. * * This function is not intended to be used directly. * * @param streamFunction - The stream function to create a stream from * @param streamValueMutator - An optional function to mutate the value emitted from the stream * @param options - The options for the stream * @param args - Additional arguments to pass to the stream function * @returns An async iterable stream proxy * @throws {StreamInvalidRetryAttemptsError} if the retryAttempts option is less than 0 and retryOnFail is true * @throws {StreamFailedError} if the stream fails and can't be restarted */ const createStream = async (streamFunction, streamValueMutator, options) => { const { onError, onFail, onRestart, onRetry, onValue, retryAttempts = DEFAULT_RETRY_ATTEMPTS, retryDelay = DEFAULT_RETRY_DELAY, retryOnFail = true, } = options ?? {}; // retry attempts must be greater than 0 if (retryOnFail && retryAttempts < 0) { throw new StreamInvalidRetryAttemptsError(); } const asyncStream = new AsyncStream(); const streamCallback = (error, value) => { // if a stream error occurs, call the onError callback if (error) { onError?.(error); return; } // ensure the value is not undefined if (value !== undefined) { try { // if a streamValueMutator is provided, mutate the value if (streamValueMutator) { const mutatedValue = streamValueMutator(value); if (isPromise(mutatedValue)) { void mutatedValue .then((mutatedValue) => { asyncStream.push(mutatedValue); onValue?.(mutatedValue); }) .catch((error) => { onError?.(error); }); } else { asyncStream.push(mutatedValue); onValue?.(mutatedValue); } } else { asyncStream.push(value); onValue?.(value); } } catch (error) { onError?.(error); } } }; const retry = async (retries = retryAttempts) => { // if the stream has been retried the maximum number of times without // success, throw an error if (retries === 0) { void asyncStream.end(); throw new StreamFailedError(retryAttempts); } // wait for the retry delay before attempting to restart the stream await wait(retryDelay); // call the onRetry callback onRetry?.(retryAttempts - retries + 1, retryAttempts); try { // attempt to restart the stream const streamCloser = await streamFunction(streamCallback, () => { // call the onFail callback onFail?.(); void retry(); }); await streamCloser.waitForReady(); // when the async stream is done, end the stream asyncStream.onDone = () => { streamCloser.end(); }; // stream restarted, call the onRestart callback onRestart?.(); } catch (error) { onError?.(error); // retry void retry(retries - 1); } }; const startRetry = () => { // if the stream should be retried, start the process if (retryOnFail) { void retry(); } else { void asyncStream.end(); // stream failed and should not be retried, throw an error throw new StreamFailedError(0); } }; try { // create the stream const streamCloser = await streamFunction(streamCallback, () => { // call the onFail callback onFail?.(); startRetry(); }); await streamCloser.waitForReady(); // when the async stream is done, end the stream asyncStream.onDone = () => { streamCloser.end(); }; } catch (error) { onError?.(error); startRetry(); } // return a proxy for the async stream return createAsyncStreamProxy(asyncStream); }; /** * Represents a conversation * * This class is not intended to be initialized directly. */ class Conversation { #client; #conversation; #lastMessage; /** * Creates a new conversation instance * * @param client - The client instance managing the conversation * @param conversation - The underlying conversation instance * @param lastMessage - Optional last message in the conversation */ constructor(client, conversation, lastMessage) { this.#client = client; this.#conversation = conversation; this.#lastMessage = lastMessage ? new DecodedMessage(client, lastMessage) : undefined; } /** * Gets the unique identifier for this conversation */ get id() { return this.#conversation.id(); } /** * Gets whether this conversation is currently active */ get isActive() { return this.#conversation.isActive(); } /** * Gets the inbox ID that added this client's inbox to the conversation */ get addedByInboxId() { return this.#conversation.addedByInboxId(); } /** * Gets the timestamp when the conversation was created in nanoseconds */ get createdAtNs() { return this.#conversation.createdAtNs(); } /** * Gets the date when the conversation was created */ get createdAt() { return nsToDate(this.createdAtNs); } /** * Gets the metadata for this conversation * * @returns Promise that resolves with the conversation metadata */ async metadata() { const metadata = await this.#conversation.groupMetadata(); return { creatorInboxId: metadata.creatorInboxId(), conversationType: metadata.conversationType(), }; } /** * Gets the members of this conversation * * @returns Promise that resolves with the conversation members */ async members() { return this.#conversation.listMembers(); } /** * Synchronizes conversation data from the network * * @returns Promise that resolves when synchronization is complete */ async sync() { return this.#conversation.sync(); } /** * Creates a stream for new messages in this conversation * * @param options - Optional stream options * @returns Stream instance for new messages */ async stream(options) { const stream = async (callback, onFail) => { await this.sync(); return this.#conversation.stream(callback, onFail); }; const convertMessage = (value) => { return new DecodedMessage(this.#client, value); }; return createStream(stream, convertMessage, options); } /** * Publishes pending messages that were sent optimistically * * @returns Promise that resolves when publishing is complete */ async publishMessages() { return this.#conversation.publishMessages(); } /** * Prepares a message to be published * * @param content - The content to send * @param contentType - Optional content type of the message content * @returns Promise that resolves with the message ID * @throws {MissingContentTypeError} if content type is required but not provided */ sendOptimistic(content, contentType) { if (typeof content !== "string" && !contentType) { throw new MissingContentTypeError(); } const encodedContent = typeof content === "string" ? this.#client.encodeContent(content, contentType ?? ContentTypeText) : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.#client.encodeContent(content, contentType); return this.#conversation.sendOptimistic(encodedContent); } /** * Publishes a new message * * @param content - The content to send * @param contentType - Optional content type of the message content * @returns Promise that resolves with the message ID after it has been sent * @throws {MissingContentTypeError} if content type is required but not provided */ async send(content, contentType) { if (typeof content !== "string" && !contentType) { throw new MissingContentTypeError(); } const encodedContent = typeof content === "string" ? this.#client.encodeContent(content, contentType ?? ContentTypeText) : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.#client.encodeContent(content, contentType); return this.#conversation.send(encodedContent); } /** * Lists messages in this conversation * * @param options - Optional filtering and pagination options * @returns Promise that resolves with an array of decoded messages */ async messages(options) { const messages = await this.#conversation.findMessages(options); return messages.map((message) => new DecodedMessage(this.#client, message)); } /** * Gets the last message in this conversation * * @returns Promise that resolves with the last message or undefined if none exists */ async lastMessage() { return this.#lastMessage ?? (await this.messages({ limit: 1 }))[0]; } /** * Gets the consent state for this conversation */ get consentState() { return this.#conversation.consentState(); } /** * Updates the consent state for this conversation * * @param consentState - The new consent state to set */ updateConsentState(consentState) { this.#conversation.updateConsentState(consentState); } /** * Gets the message disappearing settings for this conversation * * @returns The current message disappearing settings or undefined if not set */ messageDisappearingSettings() { return this.#conversation.messageDisappearingSettings() ?? undefined; } /** * Updates message disappearing settings for this conversation * * @param fromNs - The timestamp from which messages should start disappearing * @param inNs - The duration after which messages should disappear * @returns Promise that resolves when the update is complete */ async updateMessageDisappearingSettings(fromNs, inNs) { return this.#conversation.updateMessageDisappearingSettings({ fromNs, inNs, }); } /** * Removes message disappearing settings from this conversation * * @returns Promise that resolves when the settings are removed */ async removeMessageDisappearingSettings() { return this.#conversation.removeMessageDisappearingSettings(); } /** * Checks if message disappearing is enabled for this conversation * * @returns Whether message disappearing is enabled */ isMessageDisappearingEnabled() { return this.#conversation.isMessageDisappearingEnabled(); } pausedForVersion() { return this.#conversation.pausedForVersion() ?? undefined; } /** * Retrieves HMAC keys for this conversation * * @returns The HMAC keys for this conversation */ getHmacKeys() { return this.#conversation.getHmacKeys(); } /** * Retrieves information for this conversation to help with debugging * * @returns The debug information for this conversation */ async debugInfo() { return this.#conversation.debugInfo(); } } /** * Represents a direct message conversation between two inboxes * * This class is not intended to be initialized directly. */ class Dm extends Conversation { #client; #conversation; /** * Creates a new direct message conversation instance * * @param client - The client instance managing this direct message conversation * @param conversation - The underlying conversation instance * @param lastMessage - Optional last message in the conversation */ constructor(client, conversation, lastMessage) { super(client, conversation, lastMessage); this.#client = client; this.#conversation = conversation; } /** * Retrieves the inbox ID of the other participant in the DM * * @returns Promise that resolves with the peer's inbox ID */ get peerInboxId() { return this.#conversation.dmPeerInboxId(); } async getDuplicateDms() { const duplicateDms = await this.#conversation.findDuplicateDms(); return duplicateDms.map((dm) => new Dm(this.#client, dm)); } } /** * Represents a group conversation between multiple inboxes * * This class is not intended to be initialized directly. */ class Group extends Conversation { #conversation; /** * Creates a new group conversation instance * * @param client - The client instance managing this group conversation * @param conversation - The underlying conversation object * @param lastMessage - Optional last message in the conversation */ constructor(client, conversation, lastMessage) { super(client, conversation, lastMessage); this.#conversation = conversation; } /** * The name of the group */ get name() { return this.#conversation.groupName(); } /** * Updates the group's name * * @param name The new name for the group */ async updateName(name) { return this.#conversation.updateGroupName(name); } /** * The image URL of the group */ get imageUrl() { return this.#conversation.groupImageUrlSquare(); } /** * Updates the group's image URL * * @param imageUrl The new image URL for the group */ async updateImageUrl(imageUrl) { return this.#conversation.updateGroupImageUrlSquare(imageUrl); } /** * The description of the group */ get description() { return this.#conversation.groupDescription(); } /** * Updates the group's description * * @param description The new description for the group */ async updateDescription(description) { return this.#conversation.updateGroupDescription(description); } /** * The permissions of the group */ get permissions() { const permissions = this.#conversation.groupPermissions(); return { policyType: permissions.policyType(), policySet: permissions.policySet(), }; } /** * Updates a specific permission policy for the group * * @param permissionType The type of permission to update * @param policy The new permission policy * @param metadataField Optional metadata field for the permission */ async updatePermission(permissionType, policy, metadataField) { return this.#conversation.updatePermissionPolicy(permissionType, policy, metadataField); } /** * The list of admins of the group */ get admins() { return this.#conversation.adminList(); } /** * The list of super admins of the group */ get superAdmins() { return this.#conversation.superAdminList(); } /** * Checks if an inbox is an admin of the group * * @param inboxId The inbox ID to check * @returns Boolean indicating if the inbox is an admin */ isAdmin(inboxId) { return this.#conversation.isAdmin(inboxId); } /** * Checks if an inbox is a super admin of the group * * @param inboxId The inbox ID to check * @returns Boolean indicating if the inbox is a super admin */ isSuperAdmin(inboxId) { return this.#conversation.isSuperAdmin(inboxId); } /** * Adds members to the group using identifiers * * @param identifiers Array of member identifiers to add */ async addMembersByIdentifiers(identifiers) { return this.#conversation.addMembers(identifiers); } /** * Adds members to the group using inbox IDs * * @param inboxIds Array of inbox IDs to add */ async addMembers(inboxIds) { return this.#conversation.addMembersByInboxId(inboxIds); } /** * Removes members from the group using identifiers * * @param identifiers Array of member identifiers to remove */ async removeMembersByIdentifiers(identifiers) { return this.#conversation.removeMembers(identifiers); } /** * Removes members from the group using inbox IDs * * @param inboxIds Array of inbox IDs to remove */ async removeMembers(inboxIds) { return this.#conversation.removeMembersByInboxId(inboxIds); } /** * Promotes a group member to admin status * * @param inboxId The inbox ID of the member to promote */ async addAdmin(inboxId) { return this.#conversation.addAdmin(inboxId); } /** * Removes admin status from a group member * * @param inboxId The inbox ID of the admin to demote */ async removeAdmin(inboxId) { return this.#conversation.removeAdmin(inboxId); } /** * Promotes a group member to super admin status * * @param inboxId The inbox ID of the member to promote */ async addSuperAdmin(inboxId) { return this.#conversation.addSuperAdmin(inboxId); } /** * Removes super admin status from a group member * * @param inboxId The inbox ID of the super admin to demote */ async removeSuperAdmin(inboxId) { return this.#conversation.removeSuperAdmin(inboxId); } } /** * Manages conversations * * This class is not intended to be initialized directly. */ class Conversations { #client; #conversations; /** * Creates a new conversations instance * * @param client - The client instance managing the conversations * @param conversations - The underlying conversations instance */ constructor(client, conversations) { this.#client = client; this.#conversations = conversations; } /** * Retrieves a conversation by its ID * * @param id - The conversation ID to look up * @returns The conversation if found, undefined otherwise */ async getConversationById(id) { try { // findGroupById will throw if group is not found const group = this.#conversations.findGroupById(id); const metadata = await group.groupMetadata(); return metadata.conversationType() === "group" ? new Group(this.#client, group) : new Dm(this.#client, group); } catch { return undefined; } } /** * Retrieves a DM by inbox ID * * @param inboxId - The inbox ID to look up * @returns The DM if found, undefined otherwise */ getDmByInboxId(inboxId) { try { // findDmByTargetInboxId will throw if group is not found const group = this.#conversations.findDmByTargetInboxId(inboxId); return new Dm(this.#client, group); } catch { return undefined; } } /** * Retrieves a message by its ID * * @param id - The message ID to look up * @returns The decoded message if found, undefined otherwise */ getMessageById(id) { try { // findMessageById will throw if message is not found const message = this.#conversations.findMessageById(id); return new DecodedMessage(this.#client, message); } catch { return undefined; } } /** * Creates a new group conversation without syncing to the network * * @param options - Optional group creation options * @returns The new group */ newGroupOptimistic(options) { const group = this.#conversations.createGroupOptimistic(options); return new Group(this.#client, group); } /** * Creates a new group conversation with the specified identifiers * * @param identifiers - Array of identifiers for group members * @param options - Optional group creation options * @returns The new group */ async newGroupWithIdentifiers(identifiers, options) { const group = await this.#conversations.createGroup(identifiers, options); const conversation = new Group(this.#client, group); return conversation; } /** * Creates a new group conversation with the specified inbox IDs * * @param inboxIds - Array of inbox IDs for group members * @param options - Optional group creation options * @returns The new group */ async newGroup(inboxIds, options) { const group = await this.#conversations.createGroupByInboxId(inboxIds, options); const conversation = new Group(this.#client, group); return conversation; } /** * Creates a new DM conversation with the specified identifier * * @param identifier - Identifier for the DM recipient * @param options - Optional DM creation options * @returns The new DM */ async newDmWithIdentifier(identifier, options) { const group = await this.#conversations.createDm(identifier, options); const conversation = new Dm(this.#client, group); return conversation; } /** * Creates a new DM conversation with the specified inbox ID * * @param inboxId - Inbox ID for the DM recipient * @param options - Optional DM creation options * @returns The new DM */ async newDm(inboxId, options) { const group = await this.#conversations.createDmByInboxId(inboxId, options); const conversation = new Dm(this.#client, group); return conversation; } /** * Lists all conversations with optional filtering * * @param options - Optional filtering and pagination options * @returns Array of conversations */ async list(options) { const groups = this.#conversations.list(options); const conversations = await Promise.all(groups.map(async (item) => { const metadata = await item.conversation.groupMetadata(); const conversationType = metadata.conversationType(); switch (conversationType) { case "dm": return new Dm(this.#client, item.conversation, item.lastMessage); case "group": return new Group(this.#client, item.conversation, item.lastMessage); default: return undefined; } })); return conversations.filter((conversation) => conversation !== undefined); } /** * Lists all groups with optional filtering * * @param options - Optional filtering and pagination options * @returns Array of groups */ listGroups(options) { const groups = this.#conversations.list({ ...(options ?? {}), conversationType: 1 /* ConversationType.Group */, }); return groups.map((item) => { const conversation = new Group(this.#client, item.conversation, item.lastMessage); return conversation; }); } /** * Lists all DMs with optional filtering * * @param options - Optional filtering and pagination options * @returns Array of DMs */ listDms(options) { const groups = this.#conversations.list({ ...(options ?? {}), conversationType: 0 /* ConversationType.Dm */, }); return groups.map((item) => { const conversation = new Dm(this.#client, item.conversation, item.lastMessage); return conversation; }); } /** * Synchronizes conversations for the current client from the network * * @returns Promise that resolves when sync is complete */ async sync() { return this.#conversations.sync(); } /** * Synchronizes all conversations and messages from the network with optional * consent state filtering * * @param consentStates - Optional array of consent states to filter by * @returns Promise that resolves when sync is complete */ async syncAll(consentStates) { return this.#conversations.syncAllConversations(consentStates); } /** * Creates a stream for new conversations * * @param options - Optional stream options * @param options.conversationType - Optional conversation type to filter by * @returns Stream instance for new conversations */ async stream(options) { const stream = async (callback, onFail) => { await this.sync(); return this.#conversations.stream(callback, onFail, options?.conversationType); }; const convertConversation = async (value) => { const metadata = await value.groupMetadata(); const conversationType = metadata.conversationType(); let conversation; switch (conversationType) { case "dm": conversation = new Dm(this.#client, value); break; case "group": conversation = new Group(this.#client, value); break; } return conversation; }; return createStream(stream, convertConversation, options); } /** * Creates a stream for new group conversations * * @param options - Optional stream options * @returns Stream instance for new group conversations */ async streamGroups(options) { const stream = async (callback, onFail) => { await this.sync(); return this.#conversations.stream(callback, onFail, 1 /* ConversationType.Group */); }; const convertConversation = (value) => { return new Group(this.#client, value); }; return createStream(stream, convertConversation, options); } /** * Creates a stream for new DM conversations * * @param options - Optional stream options * @returns Stream instance for new DM conversations */ async streamDms(options) { const stream = async (callback, onFail) => { await this.sync(); return this.#conversations.stream(callback, onFail, 0 /* ConversationType.Dm */); }; const convertConversation = (value) => { return new Dm(this.#client, value); }; return createStream(stream, convertConversation, options); } /** * Creates a stream for all new messages * * @param options - Optional stream options * @param options.conversationType - Optional conversation type to filter by * @param options.consentStates - Optional array of consent states to filter by * @returns Stream instance for new messages */ async streamAllMessages(options) { const streamAllMessages = async (callback, onFail) => { await this.sync(); return this.#conversations.streamAllMessages(callback, onFail, options?.conversationType, options?.consentStates); }; const convertMessage = (value) => { return new DecodedMessage(this.#client, value); }; return createStream(streamAllMessages, convertMessage, options); } /** * Creates a stream for all new group messages * * @param options - Optional stream options * @param options.consentStates - Optional array of consent states to filter by * @returns Stream instance for new group messages */ async streamAllGroupMessages(options) { return this.streamAllMessages({ ...(options ?? {}), conversationType: 1 /* ConversationType.Group */, consentStates: options?.consentStates, }); } /** * Creates a stream for all new DM messages * * @param options - Optional stream options * @param options.consentStates - Optional array of consent states to filter by * @returns Stream instance for new DM messages */ async streamAllDmMessages(options) { return this.streamAllMessages({ ...(options ?? {}), conversationType: 0 /* ConversationType.Dm */, consentStates: options?.consentStates, }); } /** * Retrieves HMAC keys for all conversations * * @returns The HMAC keys for all conversations */ hmacKeys() { return this.#conversations.getHmacKeys(); } } /** * Debug information helpers for the client * * This class is not intended to be initialized directly. */ class DebugInformation { #client; #options; constructor(client, options) { this.#client = client; this.#options = options; } apiStatistics() { return this.#client.apiStatistics(); } apiIdentityStatistics() { return this.#client.apiIdentityStatistics(); } apiAggregateStatistics() { return this.#client.apiAggregateStatistics(); } clearAllStatistics() { this.#client.clearAllStatistics(); } uploadDebugArchive(serverUrl) { const env = this.#options?.env || "dev"; const historySyncUrl = this.#options?.historySyncUrl || HistorySyncUrls[env]; return this.#client.uploadDebugArchive(serverUrl || historySyncUrl); } } /** * Manages user preferences and consent states * * This class is not intended to be initialized directly. */ class Preferences { #client; #conversations; /** * Creates a new preferences instance * * @param client - The client instance managing preferences * @param conversations - The underlying conversations instance */ constructor(client, conversations) { this.#client = client; this.#conversations = conversations; } sync() { return this.#client.syncPreferences(); } /** * Retrieves the current inbox state * * @param refreshFromNetwork - Optional flag to force refresh from network * @returns Promise that resolves with the inbox state */ async inboxState(refreshFromNetwork = false) { return this.#client.inboxState(refreshFromNetwork); } /** * Gets the latest inbox state for a specific inbox * * @param inboxId - The inbox ID to get state for * @returns Promise that resolves with the latest inbox state */ async getLatestInboxState(inboxId) { return this.#client.getLatestInboxState(inboxId); } /** * Retrieves inbox state for specific inbox IDs * * @param inboxIds - Array of inbox IDs to get state for * @param refreshFromNetwork - Optional flag to force refresh from network * @returns Promise that resolves with the inbox state for the inbox IDs */ async inboxStateFromInboxIds(inboxIds, refreshFromNetwork) { return this.#client.addressesFromInboxId(refreshFromNetwork ?? false, inboxIds); } /** * Updates consent states for multiple records * * @param consentStates - Array of consent records to update * @returns Promise that resolves when consent states are updated */ async setConsentStates(consentStates) { return this.#client.setConsentStates(consentStates); } /** * Retrieves consent state for a specific entity * * @param entityType - Type of entity to get consent for * @param entity - Entity identifier * @returns Promise that resolves with the consent state */ async getConsentState(entityType, entity) { return this.#client.getConsentState(entityType, entity); } /** * Creates a stream of consent state updates * * @param options - Optional stream options * @returns Stream instance for consent updates */ streamConsent(options) { const streamConsent = async (callback, onFail) => { await this.sync(); return this.#conversations.streamConsent(callback, onFail); }; return createStream(streamConsent, undefined, options); } /** * Creates a stream of user preference updates * * @param options - Optional stream options * @returns Stream instance for preference updates */ streamPreferences(options) { const streamPreferences = async (callback, onFail) => { await this.sync(); return this.#conversations.streamPreferences(callback, onFail); }; return createStream(streamPreferences, undefined, options); } } const generateInboxId = (identifier) => { return generateInboxId$1(identifier); }; const getInboxIdForIdentifier = async (identifier, env = "dev") => { const host = ApiUrls[env]; const isSecure = host.startsWith("https"); return getInboxIdForIdentifier$1(host, isSecure, identifier); }; const createClient = async (identifier, options) => { const env = options?.env || "dev"; const host = options?.apiUrl || ApiUrls[env]; const isSecure = host.startsWith("https"); const inboxId = (await getInboxIdForIdentifier(identifier, env)) || generateInboxId(identifier); const dbPath = options?.dbPath === undefined ? join(process.cwd(), `xmtp-${env}-${inboxId}.db3`) : options.dbPath; const logOptions = { structured: options?.structuredLogging ?? false, level: options?.loggingLevel ?? "off" /* LogLevel.off */, }; const historySyncUrl = options?.historySyncUrl === undefined ? HistorySyncUrls[env] : options.historySyncUrl; const deviceSyncWorkerMode = options?.disableDeviceSync ? "disabled" /* SyncWorkerMode.disabled */ : "enabled" /* SyncWorkerMode.enabled */; return createClient$1(host, isSecure, dbPath, inboxId, identifier, options?.dbEncryptionKey, historySyncUrl, deviceSyncWorkerMode, logOptions, undefined, options?.debugEventsEnabled, options?.appVersion); }; const version = `${bindingsVersion.branch}@${bindingsVersion.version} (${bindingsVersion.date})`; /** * Client for interacting with the XMTP network */ class Client { #client; #conversations; #debugInformation; #preferences; #signer; #codecs; #identifier; #options; /** * Creates a new XMTP client instance * * This class is not intended to be initialized directly. * Use `Client.create` or `Client.build` instead. * * @param options - Optional configuration for the client */ constructor(options) { this.#options = options; const codecs = [ new GroupUpdatedCodec(), new TextCodec(), ...(options?.codecs ?? []), ]; this.#codecs = new Map(codecs.map((codec) => [codec.contentType.toString(), codec])); } /** * Initializes the client with the provided identifier * * This is not meant to be called directly. * Use `Client.create` or `Client.build` instead. * * @param identifier - The identifier to initialize the client with */ async init(identifier) { if (this.#client) { return; } this.#identifier = identifier; this.#client = await createClient(identifier, this.#options); const conversations = this.#client.conversations(); this.#conversations = new Conversations(this, conversations); this.#debugInformation = new DebugInformation(this.#client, this.#options); this.#preferences = new Preferences(this.#client, conversations); } /** * Creates a new client instance with a signer * * @param signer - The signer to use for authentication * @param options - Optional configuration for the client * @returns A new client instance */ static async create(signer, options) { const identifier = await signer.getIdentifier(); const client = new Client(options); client.#signer = signer; await client.init(identifier); if (!options?.disableAutoRegister) { await client.register(); } return client; } /** * Creates a new client instance with an identifier * * Clients created with this method must already be registered. * Any methods called that require a signer will throw an error. * * @param identifier - The identifier to use * @param options - Optional configuration for the client * @returns A new client instance */ static async build(identifier, options) { const client = new Client({ ...options, disableAutoRegister: true, }); await client.init(identifier); return client; } /** * Gets the client options */ get options() { return this.#options; } /** * Gets the signer associated with this client */ get signer() { return this.#signer; } /** * Gets the account identifier for this client */ get accountIdentifier() { return this.#identifier; } /** * Gets the inbox ID associated with this client */ get inboxId() { if (!this.#client) { throw new ClientNotInitializedError(); } return this.#client.inboxId(); } /** * Gets the installation ID for this client */ get installationId() { if (!this.#client) { throw new ClientNotInitializedError(); } return this.#client.installationId(); } /** * Gets the installation ID bytes for this client */ get installationIdBytes() { if (!this.#client) { throw new ClientNotInitializedError(); } return this.#client.installationIdBytes(); } /** * Gets whether the client is registered