UNPKG

@xmtp/node-sdk

Version:

XMTP Node client SDK for interacting with XMTP networks

1,391 lines (1,376 loc) 102 kB
import { BackendBuilder, generateInboxId as generateInboxId$1, getInboxIdByIdentity, contentTypeIntent, contentTypeActions, contentTypeWalletSendCalls, contentTypeLeaveRequest, contentTypeReadReceipt, contentTypeGroupUpdated, contentTypeTransactionReference, contentTypeMultiRemoteAttachment, contentTypeRemoteAttachment, contentTypeAttachment, contentTypeReaction, contentTypeReply, contentTypeMarkdown, contentTypeText, createClientWithBackend, revokeInstallationsSignatureRequest, applySignatureRequest, fetchInboxStatesByInboxIds, verifySignedWithPublicKey, isAddressAuthorized, isInstallationAuthorized, Backend } from '@xmtp/node-bindings'; export { ActionStyle, BackupElementSelectionOption, ConsentEntityType, ConsentState, ContentType, ConversationType, DeliveryStatus, GroupMembershipState, GroupMessageKind, GroupPermissionsOptions, IdentifierKind, ListConversationsOrderBy, LogLevel, MessageSortBy, MetadataField, PermissionLevel, PermissionPolicy, PermissionUpdateType, ReactionAction, ReactionSchema, SortDirection, WorkerKind, contentTypeActions, contentTypeAttachment, contentTypeGroupUpdated, contentTypeIntent, contentTypeLeaveRequest, contentTypeMarkdown, contentTypeMultiRemoteAttachment, contentTypeReaction, contentTypeReadReceipt, contentTypeRemoteAttachment, contentTypeReply, contentTypeText, contentTypeTransactionReference, contentTypeWalletSendCalls, decryptAttachment, encodeActions, encodeAttachment, encodeIntent, encodeMarkdown, encodeMultiRemoteAttachment, encodeReaction, encodeReadReceipt, encodeRemoteAttachment, encodeText, encodeTransactionReference, encodeWalletSendCalls, encryptAttachment, flushTelemetry, initLogging } from '@xmtp/node-bindings'; import { randomBytes } from 'node:crypto'; import { contentTypeToString } from '@xmtp/content-type-primitives'; import { isPromise } from 'node:util/types'; import { join } from 'node:path'; import process from 'node:process'; /** * Pre-configured URLs for the XMTP network based on the environment * * @deprecated Use `createBackend()` instead. * @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", "testnet-staging": "https://message-history.dev.ephemera.network", "testnet-dev": "https://message-history.dev.ephemera.network", testnet: "https://message-history.dev.ephemera.network", mainnet: "https://message-history.production.ephemera.network", }; const envMap = { local: "Local" /* BindingsEnv.Local */, dev: "Dev" /* BindingsEnv.Dev */, production: "Production" /* BindingsEnv.Production */, "testnet-staging": "TestnetStaging" /* BindingsEnv.TestnetStaging */, "testnet-dev": "TestnetDev" /* BindingsEnv.TestnetDev */, testnet: "Testnet" /* BindingsEnv.Testnet */, mainnet: "Mainnet" /* BindingsEnv.Mainnet */, }; const reverseEnvMap = { ["Local" /* BindingsEnv.Local */]: "local", ["Dev" /* BindingsEnv.Dev */]: "dev", ["Production" /* BindingsEnv.Production */]: "production", ["TestnetStaging" /* BindingsEnv.TestnetStaging */]: "testnet-staging", ["TestnetDev" /* BindingsEnv.TestnetDev */]: "testnet-dev", ["Testnet" /* BindingsEnv.Testnet */]: "testnet", ["Mainnet" /* BindingsEnv.Mainnet */]: "mainnet", }; const envToString = (env) => { return reverseEnvMap[env]; }; const createBackend = async (options) => { const env = options?.env ?? "dev"; const builder = new BackendBuilder(envMap[env]); if (options?.apiUrl) builder.setApiUrl(options.apiUrl); if (options?.gatewayHost) builder.setGatewayHost(options.gatewayHost); if (options?.appVersion) builder.setAppVersion(options.appVersion); return builder.build(); }; 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 MissingContentTypeError extends Error { constructor() { super("Content type is required when sending encoded content"); } } 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"); } } const generateInboxId = (identifier, nonce) => { return generateInboxId$1(identifier, nonce); }; const getInboxIdForIdentifier = async (backend, identifier) => { return getInboxIdByIdentity(backend, identifier); }; function isHexString(value) { return typeof value === "string" && /^0x(?:[0-9a-fA-F]{2})+$/.test(value); } function validHex(value) { if (!isHexString(value)) { throw new TypeError(`Value is not a hexadecimal string.`); } return value; } const isReaction = (m) => m.contentType.authorityId === "xmtp.org" && m.contentType.typeId === "reaction"; const isReply = (m) => m.contentType.authorityId === "xmtp.org" && m.contentType.typeId === "reply"; const isTextReply = (m) => isReply(m) && typeof m.content?.content === "string"; const isText = (m) => m.contentType.authorityId === "xmtp.org" && m.contentType.typeId === "text"; const isRemoteAttachment = (m) => m.contentType.authorityId === "xmtp.org" && m.contentType.typeId === "remoteStaticAttachment"; const isAttachment = (m) => m.contentType.authorityId === "xmtp.org" && m.contentType.typeId === "attachment"; const isMultiRemoteAttachment = (m) => m.contentType.authorityId === "xmtp.org" && m.contentType.typeId === "multiRemoteStaticAttachment"; const isTransactionReference = (m) => m.contentType.authorityId === "xmtp.org" && m.contentType.typeId === "transactionReference"; const isGroupUpdated = (m) => m.contentType.authorityId === "xmtp.org" && m.contentType.typeId === "group_updated"; const isReadReceipt = (m) => m.contentType.authorityId === "xmtp.org" && m.contentType.typeId === "readReceipt"; const isLeaveRequest = (m) => m.contentType.authorityId === "xmtp.org" && m.contentType.typeId === "leave_request"; const isWalletSendCalls = (m) => m.contentType.authorityId === "xmtp.org" && m.contentType.typeId === "walletSendCalls"; const isIntent = (m) => m.contentType.authorityId === "coinbase.com" && m.contentType.typeId === "intent"; const isActions = (m) => m.contentType.authorityId === "coinbase.com" && m.contentType.typeId === "actions"; const isMarkdown = (m) => m.contentType.authorityId === "xmtp.org" && m.contentType.typeId === "markdown"; class CodecRegistry { #codecs; constructor(codecs) { this.#codecs = new Map(codecs.map((codec) => [contentTypeToString(codec.contentType), codec])); } /** * Gets the codec for a given content type * * @param contentType - The content type to get the codec for * @returns The codec, if found */ getCodec(contentType) { return this.#codecs.get(contentTypeToString(contentType)); } } function nsToDate(ns) { return new Date(Number(ns / 1000000n)); } const getContentFromDecodedMessageContent = (content) => { switch (content.type) { case "Text" /* DecodedMessageContentType.Text */: { return content.text; } case "Markdown" /* DecodedMessageContentType.Markdown */: { return content.markdown; } case "Reply" /* DecodedMessageContentType.Reply */: { return content.reply; } case "Reaction" /* DecodedMessageContentType.Reaction */: { return content.reaction; } case "Attachment" /* DecodedMessageContentType.Attachment */: { return content.attachment; } case "RemoteAttachment" /* DecodedMessageContentType.RemoteAttachment */: { return content.remoteAttachment; } case "MultiRemoteAttachment" /* DecodedMessageContentType.MultiRemoteAttachment */: { return content.multiRemoteAttachment; } case "TransactionReference" /* DecodedMessageContentType.TransactionReference */: { return content.transactionReference; } case "GroupUpdated" /* DecodedMessageContentType.GroupUpdated */: { return content.groupUpdated; } case "ReadReceipt" /* DecodedMessageContentType.ReadReceipt */: { return content.readReceipt; } case "LeaveRequest" /* DecodedMessageContentType.LeaveRequest */: { return content.leaveRequest; } case "WalletSendCalls" /* DecodedMessageContentType.WalletSendCalls */: { return content.walletSendCalls; } case "Actions" /* DecodedMessageContentType.Actions */: { return content.actions; } case "Intent" /* DecodedMessageContentType.Intent */: { return content.intent; } case "DeletedMessage" /* DecodedMessageContentType.DeletedMessage */: { return content.deletedMessage; } case "Custom" /* DecodedMessageContentType.Custom */: { return content.custom; } default: content.type; return null; } }; const getContentTypeFromDecodedMessageContent = (content) => { switch (content.type) { case "Text" /* DecodedMessageContentType.Text */: { return contentTypeText(); } case "Markdown" /* DecodedMessageContentType.Markdown */: { return contentTypeMarkdown(); } case "Reply" /* DecodedMessageContentType.Reply */: { return contentTypeReply(); } case "Reaction" /* DecodedMessageContentType.Reaction */: { return contentTypeReaction(); } case "Attachment" /* DecodedMessageContentType.Attachment */: { return contentTypeAttachment(); } case "RemoteAttachment" /* DecodedMessageContentType.RemoteAttachment */: { return contentTypeRemoteAttachment(); } case "MultiRemoteAttachment" /* DecodedMessageContentType.MultiRemoteAttachment */: { return contentTypeMultiRemoteAttachment(); } case "TransactionReference" /* DecodedMessageContentType.TransactionReference */: { return contentTypeTransactionReference(); } case "GroupUpdated" /* DecodedMessageContentType.GroupUpdated */: { return contentTypeGroupUpdated(); } case "ReadReceipt" /* DecodedMessageContentType.ReadReceipt */: { return contentTypeReadReceipt(); } case "LeaveRequest" /* DecodedMessageContentType.LeaveRequest */: { return contentTypeLeaveRequest(); } case "WalletSendCalls" /* DecodedMessageContentType.WalletSendCalls */: { return contentTypeWalletSendCalls(); } case "Actions" /* DecodedMessageContentType.Actions */: { return contentTypeActions(); } case "Intent" /* DecodedMessageContentType.Intent */: { return contentTypeIntent(); } case "DeletedMessage" /* DecodedMessageContentType.DeletedMessage */: { return undefined; } case "Custom" /* DecodedMessageContentType.Custom */: { return content.custom?.type; } default: content.type; return undefined; } }; /** * Represents a decoded XMTP message * * @class * @property {unknown} 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 {bigint} expiresAtNs - Timestamp when the message will expire (in nanoseconds) * @property {Date} expiresAt - Timestamp when the message will expire * @property {string} [fallback] - Optional fallback text for the message * @property {string} id - Unique identifier for the message * @property {MessageKind} kind - Type of message ("application" | "membership_change") * @property {number} numReplies - Number of replies to the message * @property {DecodedMessage<Reaction>[]} reactions - Reactions to the message * @property {string} senderInboxId - Identifier for the sender's inbox * @property {Date} sentAt - Timestamp when the message was sent * @property {bigint} sentAtNs - Timestamp when the message was sent (in nanoseconds) */ class DecodedMessage { content; contentType; conversationId; deliveryStatus; expiresAtNs; expiresAt; fallback; id; kind; numReplies; reactions; senderInboxId; sentAt; sentAtNs; constructor(codecRegistry, message) { this.id = message.id; this.expiresAtNs = message.expiresAtNs ?? undefined; this.expiresAt = message.expiresAtNs ? nsToDate(message.expiresAtNs) : undefined; this.sentAtNs = message.sentAtNs; this.sentAt = nsToDate(message.sentAtNs); this.conversationId = message.conversationId; this.senderInboxId = message.senderInboxId; this.contentType = message.contentType; this.fallback = message.fallback ?? undefined; this.kind = message.kind; this.deliveryStatus = message.deliveryStatus; this.numReplies = message.numReplies; this.reactions = message.reactions.map((reaction) => new DecodedMessage(codecRegistry, reaction)); this.content = getContentFromDecodedMessageContent(message.content) ?? undefined; switch (message.content.type) { case "Reply" /* DecodedMessageContentType.Reply */: { const reply = message.content.reply; let replyContent = getContentFromDecodedMessageContent(reply.content); if (reply.content.type === "Custom" /* DecodedMessageContentType.Custom */) { const codec = codecRegistry.getCodec(reply.content.custom?.type); if (codec) { try { replyContent = codec.decode(replyContent); } catch (error) { if (error instanceof Error) { console.warn(`Error decoding custom content: ${error.message}`); } else { console.warn(`Error decoding custom content`); } } } } this.content = { referenceId: reply.referenceId, content: replyContent, contentType: getContentTypeFromDecodedMessageContent(reply.content), inReplyTo: reply.inReplyTo ? new DecodedMessage(codecRegistry, reply.inReplyTo) : null, }; break; } case "Custom" /* DecodedMessageContentType.Custom */: { const customContent = message.content.custom; if (customContent !== null) { const codec = codecRegistry.getCodec(this.contentType); if (codec) { try { this.content = codec.decode(customContent); } catch (error) { if (error instanceof Error) { console.warn(`Error decoding custom content: ${error.message}`); } else { console.warn(`Error decoding custom content`); } this.content = undefined; } } else { console.warn(`No codec found for content type "${contentTypeToString(this.contentType)}"`); this.content = undefined; } } break; } } } } /** * 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 = 60_000; // milliseconds const DEFAULT_RETRY_ATTEMPTS = 10; /** * 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) => { if (mutatedValue !== undefined) { asyncStream.push(mutatedValue); onValue?.(mutatedValue); } }) .catch((error) => { onError?.(error); }); } else { if (mutatedValue !== undefined) { 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, call onError if (retries === 0) { void asyncStream.end(); onError?.(new StreamFailedError(retryAttempts)); return; } // 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 onError?.(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; #codecRegistry; #conversation; /** * Creates a new conversation instance * * @param client - The client instance managing the conversation * @param codecRegistry - The codec registry instance * @param conversation - The underlying conversation instance */ constructor(client, codecRegistry, conversation) { this.#client = client; this.#codecRegistry = codecRegistry; this.#conversation = conversation; } /** * 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); } get topic() { return `/xmtp/mls/1/g-${this.id}/proto`; } pausedForVersion() { return this.#conversation.pausedForVersion() ?? undefined; } /** * Gets HMAC keys for this conversation * * @returns The HMAC keys for this conversation */ hmacKeys() { return this.#conversation.hmacKeys(); } /** * 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) => { if (!options?.disableSync) { await this.sync(); } return this.#conversation.stream(callback, onFail); }; const convertMessage = (value) => { const enrichedMessage = this.#client.conversations.getMessageById(value.id); if (enrichedMessage === undefined) { console.warn(`Streamed message with ID "${value.id}" not found`); } return enrichedMessage; }; return createStream(stream, convertMessage, options); } /** * Decodes, decrypts, and persists a raw envelope from a group message stream. * * @param envelopeBytes - Raw protobuf-encoded envelope bytes from the stream * @returns The processed and stored messages */ async processStreamedMessage(envelopeBytes) { return this.#conversation.processStreamedGroupMessage(envelopeBytes); } /** * Publishes pending messages that were sent optimistically * * @returns Promise that resolves when publishing is complete */ async publishMessages() { return this.#conversation.publishMessages(); } /** * Sends a message with configurable delivery behavior * * @param encodedContent - The encoded content to send * @param sendOptions - Options for sending the message * @param sendOptions.shouldPush - Indicates whether this message should be * included in push notifications * @param sendOptions.optimistic - Indicates whether this message should be * sent optimistically and published later via `publishMessages` * @param sendOptions.idempotencyKey - Optional idempotency key; re-sending * identical content with the same key produces the same deduplicated message id * @returns Promise that resolves with the message ID after it has been sent */ async send(encodedContent, sendOptions) { if (!encodedContent.type) { throw new MissingContentTypeError(); } return this.#conversation.send(encodedContent, sendOptions ?? { shouldPush: false }); } /** * Sends a text message * * @param text - The text to send * @param opts - Send options (optimistic delivery, idempotency key) * @returns Promise that resolves with the message ID after it has been sent */ async sendText(text, opts) { return this.#conversation.sendText(text, opts); } /** * Sends a markdown message * * @param markdown - The markdown to send * @param opts - Send options (optimistic delivery, idempotency key) * @returns Promise that resolves with the message ID after it has been sent */ async sendMarkdown(markdown, opts) { return this.#conversation.sendMarkdown(markdown, opts); } /** * Sends a reaction message * * @param reaction - The reaction to send * @param opts - Send options (optimistic delivery, idempotency key) * @returns Promise that resolves with the message ID after it has been sent */ async sendReaction(reaction, opts) { return this.#conversation.sendReaction(reaction, opts); } /** * Sends a read receipt message * * @param opts - Send options (optimistic delivery, idempotency key) * @returns Promise that resolves with the message ID after it has been sent */ async sendReadReceipt(opts) { return this.#conversation.sendReadReceipt(opts); } /** * Sends a reply message * * @param reply - The reply to send * @param opts - Send options (optimistic delivery, idempotency key) * @returns Promise that resolves with the message ID after it has been sent */ async sendReply(reply, opts) { return this.#conversation.sendReply(reply, opts); } /** * Sends a transaction reference message * * @param transactionReference - The transaction reference to send * @param opts - Send options (optimistic delivery, idempotency key) * @returns Promise that resolves with the message ID after it has been sent */ async sendTransactionReference(transactionReference, opts) { return this.#conversation.sendTransactionReference(transactionReference, opts); } /** * Sends a wallet send calls message * * @param walletSendCalls - The wallet send calls to send * @param opts - Send options (optimistic delivery, idempotency key) * @returns Promise that resolves with the message ID after it has been sent */ async sendWalletSendCalls(walletSendCalls, opts) { return this.#conversation.sendWalletSendCalls(walletSendCalls, opts); } /** * Sends a actions message * * @param actions - The actions to send * @param opts - Send options (optimistic delivery, idempotency key) * @returns Promise that resolves with the message ID after it has been sent */ async sendActions(actions, opts) { return this.#conversation.sendActions(actions, opts); } /** * Sends a intent message * * @param intent - The intent to send * @param opts - Send options (optimistic delivery, idempotency key) * @returns Promise that resolves with the message ID after it has been sent */ async sendIntent(intent, opts) { return this.#conversation.sendIntent(intent, opts); } /** * Sends an attachment message * * @param attachment - The attachment to send * @param opts - Send options (optimistic delivery, idempotency key) * @returns Promise that resolves with the message ID after it has been sent */ async sendAttachment(attachment, opts) { return this.#conversation.sendAttachment(attachment, opts); } /** * Sends a multi remote attachment message * * @param multiRemoteAttachment - The multi remote attachment to send * @param opts - Send options (optimistic delivery, idempotency key) * @returns Promise that resolves with the message ID after it has been sent */ async sendMultiRemoteAttachment(multiRemoteAttachment, opts) { return this.#conversation.sendMultiRemoteAttachment(multiRemoteAttachment, opts); } /** * Sends a remote attachment message * * @param remoteAttachment - The remote attachment to send * @param opts - Send options (optimistic delivery, idempotency key) * @returns Promise that resolves with the message ID after it has been sent */ async sendRemoteAttachment(remoteAttachment, opts) { return this.#conversation.sendRemoteAttachment(remoteAttachment, opts); } /** * 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.listEnrichedMessages(options); return messages.map((message) => new DecodedMessage(this.#codecRegistry, message)); } /** * Counts messages in this conversation * * @param options - Optional filtering options * @returns Promise that resolves with the count of messages */ async countMessages(options) { const count = await this.#conversation.countMessages(options); return count; } /** * Gets the last message in this conversation * * @returns Promise that resolves with the last message or undefined if none exists */ async lastMessage() { const messages = await this.messages({ limit: 1, direction: 1 /* SortDirection.Descending */, }); if (messages.length > 0) { return messages[0]; } return undefined; } /** * Gets the consent state for this conversation */ 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(); } /** * Retrieves information for this conversation to help with debugging * * @returns The debug information for this conversation */ async debugInfo() { return this.#conversation.debugInfo(); } /** * Retrieves the last read times for this conversation * * @returns A map keyed by inbox ID with the last read timestamp * (nanoseconds since epoch) */ async lastReadTimes() { return this.#conversation.lastReadTimes(); } } /** * Represents a direct message conversation between two inboxes * * This class is not intended to be initialized directly. */ class Dm extends Conversation { #client; #codecRegistry; #conversation; /** * Creates a new direct message conversation instance * * @param client - The client instance managing this direct message conversation * @param codecRegistry - The codec registry instance * @param conversation - The underlying conversation instance */ constructor(client, codecRegistry, conversation) { super(client, codecRegistry, conversation); this.#client = client; this.#codecRegistry = codecRegistry; 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 duplicateDms() { const duplicateDms = await this.#conversation.duplicateDms(); return duplicateDms.map((dm) => new Dm(this.#client, this.#codecRegistry, 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 codecRegistry - The codec registry instance * @param conversation - The underlying conversation object */ constructor(client, codecRegistry, conversation) { super(client, codecRegistry, conversation); 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 app data of the group */ get appData() { return this.#conversation.appData(); } /** * Updates the group's app data (max 8192 bytes) * * @param appData The new app data for the group */ async updateAppData(appData) { return this.#conversation.updateAppData({ value: appData }); } /** * The permissions of the group */ 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 */ listAdmins() { return this.#conversation.listAdmins(); } /** * The list of super admins of the group */ listSuperAdmins() { return this.#conversation.listSuperAdmins(); } /** * 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.addMembersByIdentity(identifiers); } /** * Adds members to the group using inbox IDs * * @param inboxIds Array of inbox IDs to add */ async addMembers(inboxIds) { return this.#conversation.addMembers(inboxIds); } /** * Removes members from the group using identifiers * * @param identifiers Array of member identifiers to remove */ async removeMembersByIdentifiers(identifiers) { return this.#conversation.removeMembersByIdentity(identifiers); } /** * Removes members from the group using inbox IDs * * @param inboxIds Array of inbox IDs to remove */ async removeMembers(inboxIds) { return this.#conversation.removeMembers(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); } /** * Request to leave the group */ async requestRemoval() { return this.#conversation.leaveGroup(); } /** * Checks if the current user has requested to leave the group * * @returns Boolean */ isPendingRemoval() { return (this.#conversation.membershipState() === 4 /* GroupMembershipState.PendingRemove */); } } /** * Manages conversations * * This class is not intended to be initialized directly. */ class Conversations { #client; #codecRegistry; #conversations; /** * Creates a new conversations instance * * @param client - The client instance managing the conversations * @param codecRegistry - The codec registry instance * @param conversations - The underlying conversations instance */ constructor(client, codecRegistry, conversations) { this.#client = client; this.#codecRegistry = codecRegistry; this.#conversations = conversations; } get topic() { return `/xmtp/mls/1/w-${this.#client.installationId}/proto`; } /** * Retrieves a conversation by its ID * * @param id - The conversation ID to look up * @returns The conversation if found, undefined otherwise * @see https://docs.xmtp.org/chat-apps/core-messaging/create-conversations#conversation-helper-methods */ async getConversationById(id) { try { // getConversationById will throw if group is not found const group = this.#conversations.getConversationById(id); const metadata = await group.groupMetadata(); switch (metadata.conversationType()) { case 1 /* ConversationType.Group */: return new Group(this.#client, this.#codecRegistry, group); case 0 /* ConversationType.Dm */: return new Dm(this.#client, this.#codecRegistry, group); default: return undefined; } } catch { return undefined; } } /** * Retrieves a DM by inbox ID * * @param inboxId - The inbox ID to look up * @returns The DM if found, undefined otherwise * @see https://docs.xmtp.org/chat-apps/core-messaging/create-conversations#conversation-helper-methods */ getDmByInboxId(inboxId) { try { // getDmByInboxId will throw if group is not found const group = this.#conversations.getDmByInboxId(inboxId); return new Dm(this.#client, this.#codecRegistry, group); } catch { return undefined; } } /** * Retrieves a DM by identifier * * @param identifier - The identifier to look up * @returns Promise that resolves with the DM, if found * @see https://docs.xmtp.org/chat-apps/core-messaging/create-conversations#conversation-helper-methods */ async fetchDmByIdentifier(identifier) { const inboxId = await this.#client.fetchInboxIdByIdentifier(identifier); if (!inboxId) { return undefined; } return this.getDmByInboxId(inboxId); } /** * Retrieves a message by its ID * * @param id - The message ID to look up * @returns The decoded message if found, undefined otherwise * @see https://docs.xmtp.org/chat-apps/core-messaging/create-conversations#conversation-helper-methods */ getMessageById(id) { try { // getEnrich