Skip to content
← Public packages

@kentcdodds/groupme

GroupMe REST API helpers for listing groups, reading messages, ranking activity, and preparing relevance-filtered digests for Kody agents.

src/request.ts

76 lines · 3.2 KB · TypeScript
import type {
	GroupMeEnvelope,
	GroupMeHttpMethod,
	GroupMeRequestOptions,
	GroupMeRequestResponse,
} from './types.ts'

export const GROUPME_API_BASE_URL = 'https://api.groupme.com/v3'
export const GROUPME_INTEGRATION = 'groupme'
export const GROUPME_ACCESS_TOKEN_SECRET = 'groupmeAccessToken'
export const GROUPME_CLIENT_ID_VALUE = 'groupme-client-id'

/** Error thrown when a GroupMe HTTP request fails. */
export class GroupMeRequestError extends Error {
	response: GroupMeRequestResponse
	constructor(response: GroupMeRequestResponse) {
		const errors = (response.data as GroupMeEnvelope<unknown> | null)?.meta?.errors
		super(
			Array.isArray(errors) && errors.length
				? `GroupMe request failed with ${response.status}: ${errors.join(', ')}`
				: `GroupMe request failed with ${response.status} ${response.statusText}`,
		)
		this.name = 'GroupMeRequestError'
		this.response = response
	}
}

/**
 * Compact authenticated GroupMe v3 request helper (~30 lines of plumbing).
 * Auth: `X-Access-Token` via `{{secret:groupmeAccessToken|scope=user}}`.
 */
export async function request<T = unknown>(
	options: GroupMeRequestOptions,
): Promise<GroupMeRequestResponse<T>> {
	if (/^https?:\/\//i.test(options.path) && !options.path.startsWith(GROUPME_API_BASE_URL)) {
		throw new Error('GroupMe toolkit only accepts api.groupme.com/v3 URLs or API paths.')
	}
	const base = options.path.startsWith(GROUPME_API_BASE_URL)
		? options.path
		: `${GROUPME_API_BASE_URL}${options.path.startsWith('/') ? '' : '/'}${options.path}`
	const qs = new URLSearchParams()
	for (const [k, v] of Object.entries(options.query ?? {})) {
		if (v == null) continue
		for (const item of Array.isArray(v) ? v : [v]) qs.append(k, String(item))
	}
	const q = qs.toString()
	const url = q ? `${base}${base.includes('?') ? '&' : '?'}${q}` : base
	const headers = new Headers(options.headers)
	headers.set('X-Access-Token', `{{secret:${GROUPME_ACCESS_TOKEN_SECRET}|scope=user}}`)
	if (!headers.has('Accept')) headers.set('Accept', 'application/json')
	const method = (options.method ?? (options.body === undefined ? 'GET' : 'POST')).toUpperCase() as GroupMeHttpMethod
	const init: RequestInit = { method, headers }
	if (options.body !== undefined) {
		if (!headers.has('Content-Type')) headers.set('Content-Type', 'application/json')
		init.body = typeof options.body === 'string' ? options.body : JSON.stringify(options.body)
	}
	const res = await fetch(url, init)
	const text = await res.text()
	let data: GroupMeRequestResponse<T>['data'] = text || null
	if (text && (text.startsWith('{') || text.startsWith('[') || (res.headers.get('content-type') ?? '').includes('json'))) {
		try { data = JSON.parse(text) } catch { /* keep text */ }
	}
	const response: GroupMeRequestResponse<T> = { url, ok: res.ok, status: res.status, statusText: res.statusText, data, text }
	if (!response.ok && options.throwOnError !== false) throw new GroupMeRequestError(response)
	return response
}

/**
 * Perform a low-level authenticated GroupMe v3 API request.
 * @example
 * import request from 'kody:@kentcdodds/groupme/request'
 * const response = await request({ path: '/groups', throwOnError: true })
 */
export default async function requestEntrypoint(params: GroupMeRequestOptions) {
	return await request(params)
}