← 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 · TypeScriptimport 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)
}