Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/twilio

src/send-message.ts

89 lines · 2.5 KB · TypeScript
import { twilioRequest } from './client.ts'
import { mutationPreview } from './helpers.ts'
import { resolveMessagingServiceSid } from './config.ts'
import {
	assertE164,
	inputRecord,
	optionalE164,
	requiredString,
	type TwilioAuthOptions,
} from './validation.ts'

/**
 * Preview or send an SMS.
 *
 * Defaults to dry-run. A live send requires `confirm: true` after the user
 * explicitly approves the destination and body. Never bakes in a from/to
 * number — pass `from` or a Messaging Service SID (param or packageStorage).
 */
export async function sendMessage(
	params: TwilioAuthOptions & {
		to: string
		body: string
		from?: string
		messagingServiceSid?: string
		statusCallback?: string
		dryRun?: boolean
		confirm?: boolean
	},
) {
	const input = inputRecord(params)
	const to = assertE164(requiredString(input, 'to'), 'to')
	const body = requiredString(input, 'body')
	const from = optionalE164(input, 'from')
	const messagingServiceSid = await resolveMessagingServiceSid(input)
	const statusCallback =
		typeof input.statusCallback === 'string' && input.statusCallback.trim()
			? input.statusCallback.trim()
			: undefined

	if (statusCallback && !/^https:\/\//i.test(statusCallback)) {
		throw new Error('statusCallback must be an https URL when provided.')
	}

	if (!from && !messagingServiceSid) {
		if (input.confirm === true && input.dryRun !== true) {
			throw new Error(
				'Sending SMS requires from (E.164) or messagingServiceSid (MG...). Pass one explicitly or store a Messaging Service SID in packageStorage after you fork. This package has no baked-in phone numbers.',
			)
		}
	}

	const payload = {
		To: to,
		Body: body,
		From: from,
		MessagingServiceSid: messagingServiceSid,
		StatusCallback: statusCallback,
	}

	const preview = mutationPreview(input, '/Messages.json', payload)
	if (preview) {
		return {
			...preview,
			to,
			from: from ?? null,
			messagingServiceSid: messagingServiceSid ?? null,
			bodyLength: body.length,
			missingSender: !from && !messagingServiceSid,
		}
	}

	const raw = await twilioRequest({
		...input,
		resource: '/Messages.json',
		method: 'POST',
		body: payload,
	})

	return {
		ok: true,
		sid: typeof raw.sid === 'string' ? raw.sid : null,
		status: typeof raw.status === 'string' ? raw.status : null,
		to: typeof raw.to === 'string' ? raw.to : to,
		from: typeof raw.from === 'string' ? raw.from : from ?? null,
		dateCreated: typeof raw.date_created === 'string' ? raw.date_created : null,
	}
}

export default sendMessage