From 3fb0dfb617fd7641e50779a992be45d252072fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicole=20Miko=C5=82ajczyk?= Date: Wed, 9 Apr 2025 13:41:32 +0200 Subject: [PATCH] pl-api: untested imethods for mitra subscriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicole Mikołajczyk --- packages/pl-api/lib/client.ts | 207 +++++++++++++----- packages/pl-api/lib/entities/account.ts | 28 +++ packages/pl-api/lib/entities/index.ts | 3 + .../lib/entities/subscription-details.ts | 23 ++ .../lib/entities/subscription-invoice.ts | 37 ++++ .../lib/entities/subscription-option.ts | 25 +++ 6 files changed, 271 insertions(+), 52 deletions(-) create mode 100644 packages/pl-api/lib/entities/subscription-details.ts create mode 100644 packages/pl-api/lib/entities/subscription-invoice.ts create mode 100644 packages/pl-api/lib/entities/subscription-option.ts diff --git a/packages/pl-api/lib/client.ts b/packages/pl-api/lib/client.ts index bdf198b88..bf3570237 100644 --- a/packages/pl-api/lib/client.ts +++ b/packages/pl-api/lib/client.ts @@ -70,6 +70,9 @@ import { statusSchema, statusSourceSchema, streamingEventSchema, + subscriptionDetailsSchema, + subscriptionInvoiceSchema, + subscriptionOptionSchema, suggestionSchema, tagSchema, tokenSchema, @@ -2859,58 +2862,6 @@ class PlApiClient { }, }; - public readonly shoutbox = { - connect: (token: string, { onMessage, onMessages }: { - onMessages: (messages: Array) => void; - onMessage: (message: ShoutMessage) => void; - }) => { - let counter = 2; - let intervalId: NodeJS.Timeout; - if (this.#shoutSocket) return this.#shoutSocket; - - const path = buildFullPath('/socket/websocket', this.baseURL, { token, vsn: '2.0.0' }); - - const ws = new WebSocket(path); - - ws.onmessage = (event) => { - const [_, __, ___, type, payload] = JSON.parse(event.data as string); - if (type === 'new_msg') { - const message = v.parse(shoutMessageSchema, payload); - onMessage(message); - } else if (type === 'messages') { - const messages = v.parse(filteredArray(shoutMessageSchema), payload.messages); - onMessages(messages); - } - }; - - ws.onopen = () => { - ws.send(JSON.stringify(['3', `${++counter}`, 'chat:public', 'phx_join', {}])); - - intervalId = setInterval(() => { - ws.send(JSON.stringify([null, `${++counter}`, 'phoenix', 'heartbeat', {}])); - }, 5000); - }; - - ws.onclose = () => { - clearInterval(intervalId); - }; - - this.#shoutSocket = { - message: (text: string) => { - // guess this is meant to be incremented on each call but idk - ws.send(JSON.stringify(['3', `${++counter}`, 'chat:public', 'new_msg', { 'text': text }])); - }, - close: () => { - ws.close(); - this.#shoutSocket = undefined; - clearInterval(intervalId); - }, - }; - - return this.#shoutSocket; - }, - }; - public readonly notifications = { /** * Get all notifications @@ -4739,6 +4690,58 @@ class PlApiClient { }, }; + public readonly shoutbox = { + connect: (token: string, { onMessage, onMessages }: { + onMessages: (messages: Array) => void; + onMessage: (message: ShoutMessage) => void; + }) => { + let counter = 2; + let intervalId: NodeJS.Timeout; + if (this.#shoutSocket) return this.#shoutSocket; + + const path = buildFullPath('/socket/websocket', this.baseURL, { token, vsn: '2.0.0' }); + + const ws = new WebSocket(path); + + ws.onmessage = (event) => { + const [_, __, ___, type, payload] = JSON.parse(event.data as string); + if (type === 'new_msg') { + const message = v.parse(shoutMessageSchema, payload); + onMessage(message); + } else if (type === 'messages') { + const messages = v.parse(filteredArray(shoutMessageSchema), payload.messages); + onMessages(messages); + } + }; + + ws.onopen = () => { + ws.send(JSON.stringify(['3', `${++counter}`, 'chat:public', 'phx_join', {}])); + + intervalId = setInterval(() => { + ws.send(JSON.stringify([null, `${++counter}`, 'phoenix', 'heartbeat', {}])); + }, 5000); + }; + + ws.onclose = () => { + clearInterval(intervalId); + }; + + this.#shoutSocket = { + message: (text: string) => { + // guess this is meant to be incremented on each call but idk + ws.send(JSON.stringify(['3', `${++counter}`, 'chat:public', 'new_msg', { 'text': text }])); + }, + close: () => { + ws.close(); + this.#shoutSocket = undefined; + clearInterval(intervalId); + }, + }; + + return this.#shoutSocket; + }, + }; + public readonly events = { /** * Creates an event @@ -5250,6 +5253,106 @@ class PlApiClient { }, }; + public readonly subscriptions = { + /** + * Add subscriber or extend existing subscription. + * + * Requires features{@link Features['subscriptions']}. + * @param subscriberId - The subscriber ID. + * @param duration - The subscription duration (in seconds). + */ + createSubscription: async(subscriberId: string, duration: number) => { + const response = await this.request('/api/v1/subscriptions', { method: 'POST', body: { subscriber_id: subscriberId, duration } }); + + return v.parse(subscriptionDetailsSchema, response.json); + }, + + /** + * Get list of subscription options + * + * Requires features{@link Features['subscriptions']}. + */ + getSubscriptionOptions: async () => { + const response = await this.request('/api/v1/subscriptions/options'); + + return v.parse(filteredArray(subscriptionOptionSchema), response.json); + }, + + /** + * Enable subscriptions or update subscription settings + * + * Requires features{@link Features['subscriptions']}. + * @param type - Subscription type + * @param chainId - CAIP-2 chain ID. + * @param price - Subscription price (only for Monero) + * @param payoutAddress - Payout address (only for Monero) + */ + updateSubscription: async(type: 'monero', chainId?: string, price?: number, payoutAddress?: string) => { + const response = await this.request('/api/v1/subscriptions/options', { method: 'POST', body: { type, chain_id: chainId, price, payout_address: payoutAddress } }); + + return v.parse(accountSchema, response.json); + }, + + /** + * Find subscription by sender and recipient + * + * Requires features{@link Features['subscriptions']}. + * @param senderId - Sender ID. + * @param recipientId - Recipient ID. + */ + findSubscription: async(senderId: string, recipientId: string) => { + const response = await this.request('/api/v1/subscriptions/find', { method: 'POST', body: { sender_id: senderId, recipient_id: recipientId } }); + + return v.parse(subscriptionDetailsSchema, response.json); + }, + + /** + * Create invoice + * + * Requires features{@link Features['subscriptions']}. + * @param senderId - Sender ID. + * @param recipientId - Recipient ID. + * @param chainId - CAIP-2 chain ID. + * @param amount - Requested payment amount (in atomic units). + */ + createInvoice: async(senderId: string, recipientId: string, chainId: string, amount: number) => { + const response = await this.request('/api/v1/subscriptions/invoices', { + method: 'POST', + body: { + sender_id: senderId, recipient_id: recipientId, chain_id: chainId, amount, + }, + }); + + return v.parse(subscriptionInvoiceSchema, response.json); + }, + + /** + * View information about an invoice. + * + * Requires features{@link Features['invoices']}. + * @param invoiceId - Invoice ID + */ + getInvoice: async(invoiceId: string) => { + const response = await this.request(`/api/v1/subscriptions/invoices/${invoiceId}`); + + return v.parse(subscriptionInvoiceSchema, response.json); + }, + + /** + * Cancel invoice. + * + * Requires features{@link Features['invoices']}. + * @param invoiceId - Invoice ID + */ + cancelInvoice: async(invoiceId: string) => { + const response = await this.request(`/api/v1/subscriptions/invoices/${invoiceId}`, { + method: 'DELETE', + }); + + return v.parse(subscriptionInvoiceSchema, response.json); + }, + }; + /** Routes that are not part of any stable release */ public readonly experimental = { admin: { diff --git a/packages/pl-api/lib/entities/account.ts b/packages/pl-api/lib/entities/account.ts index 8393d939a..9b91db11e 100644 --- a/packages/pl-api/lib/entities/account.ts +++ b/packages/pl-api/lib/entities/account.ts @@ -158,10 +158,38 @@ const baseAccountSchema = v.object({ pronouns: v.fallback(v.array(v.string()), []), + /** Mention policy */ mention_policy: v.fallback(v.picklist(['none', 'only_known', 'only_followers']), 'none'), + /** The reported subscribers of this user */ subscribers_count: v.fallback(v.number(), 0), + /** Identity proofs */ + identity_proofs: filteredArray(v.object({ + /** The key of a given field's key-value pair */ + name: v.fallback(v.string(), ''), + /** The value associated with the name key */ + value: v.fallback(v.string(), ''), + /** Timestamp of when the server verified the field value */ + verified_at: v.fallback(datetimeSchema, new Date().toISOString()), + })), + /** Payment options */ + payment_options: filteredArray(v.object({ + /** Payment type */ + type: v.picklist(['link', 'monero-subscription']), + /** Link name (only for link type) */ + name: v.fallback(v.nullable(v.string()), null), + /** Link URL (only for link type) */ + href: v.fallback(v.nullable(v.string()), null), + /** CAIP-2 chain ID (only for monero-subscription type) */ + chain_id: v.fallback(v.nullable(v.string()), null), + /** Subscription price (only for monero-subscription type) */ + price: v.fallback(v.nullable(v.string()), null), + /** Unique identifier of a proposal object */ + object_id: v.fallback(v.nullable(v.string()), null), + })), + /** Whether the user is a cat */ is_cat: v.fallback(v.boolean(), false), + /** Whether the user's posts should be nyanified */ speak_as_cat: v.fallback(v.boolean(), false), __meta: coerceObject({ diff --git a/packages/pl-api/lib/entities/index.ts b/packages/pl-api/lib/entities/index.ts index b4a897135..e17a3d4bf 100644 --- a/packages/pl-api/lib/entities/index.ts +++ b/packages/pl-api/lib/entities/index.ts @@ -72,6 +72,9 @@ export * from './status'; export * from './status-edit'; export * from './status-source'; export * from './streaming-event'; +export * from './subscription-details'; +export * from './subscription-invoice'; +export * from './subscription-option'; export * from './suggestion'; export * from './tag'; export * from './token'; diff --git a/packages/pl-api/lib/entities/subscription-details.ts b/packages/pl-api/lib/entities/subscription-details.ts new file mode 100644 index 000000000..1bb5fd8a0 --- /dev/null +++ b/packages/pl-api/lib/entities/subscription-details.ts @@ -0,0 +1,23 @@ +import * as v from 'valibot'; + +import { datetimeSchema } from './utils'; + +/** + * @category Schemas + */ +const subscriptionDetailsSchema = v.object({ + /** Subscription ID. */ + id: v.number(), + /** The date when subscription expires. */ + expires_at: v.fallback(datetimeSchema, new Date().toISOString()), +}); + +/** + * @category Entity types + */ +type SubscriptionDetails = v.InferOutput; + +export { + subscriptionDetailsSchema, + type SubscriptionDetails, +}; diff --git a/packages/pl-api/lib/entities/subscription-invoice.ts b/packages/pl-api/lib/entities/subscription-invoice.ts new file mode 100644 index 000000000..6646de27e --- /dev/null +++ b/packages/pl-api/lib/entities/subscription-invoice.ts @@ -0,0 +1,37 @@ +import * as v from 'valibot'; + +import { datetimeSchema } from './utils'; + +/** + * @category Schemas + */ +const subscriptionInvoiceSchema = v.object({ + /** Invoice ID. */ + invoice_id: v.string(), + /** The ID of the sender. */ + sender_id: v.string(), + /** The ID of the recipient. */ + recipient_id: v.string(), + /** CAIP-2 chain ID. */ + chain_id: v.string(), + /** Payment address. */ + payment_address: v.string(), + /** Requested payment amount (in atomic units). */ + amount: v.number(), + /** Invoice status. */ + status: v.picklist(['open', 'paid', 'forwarded', 'timeout', 'cancelled', 'underpaid', 'completed', 'failed']), + /** The date when invoice was created. */ + created_at: v.fallback(datetimeSchema, new Date().toISOString()), + /** The date when invoice times out. */ + invoice_expires_at: v.fallback(datetimeSchema, new Date().toISOString()), +}); + +/** + * @category Entity types + */ +type SubscriptionInvoice = v.InferOutput; + +export { + subscriptionInvoiceSchema, + type SubscriptionInvoice, +}; diff --git a/packages/pl-api/lib/entities/subscription-option.ts b/packages/pl-api/lib/entities/subscription-option.ts new file mode 100644 index 000000000..156a994fc --- /dev/null +++ b/packages/pl-api/lib/entities/subscription-option.ts @@ -0,0 +1,25 @@ +import * as v from 'valibot'; + +/** + * @category Schemas + */ +const subscriptionOptionSchema = v.object({ + /** Subscription type */ + type: v.picklist(['monero']), + /** CAIP-2 chain ID. */ + chain_id: v.fallback(v.string(), ''), + /** Subscription price (only for Monero) */ + price: v.fallback(v.nullable(v.number()), null), + /** Payout address (only for Monero) */ + payout_address: v.fallback(v.string(), ''), +}); + +/** + * @category Entity types + */ +type SubscriptionOption = v.InferOutput; + +export { + subscriptionOptionSchema, + type SubscriptionOption, +};