Skip to content

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

Package listing

@kentcdodds/slack

src/request.ts

74 lines · 2.1 KB · TypeScript
import { createAuthenticatedFetch } from 'kody:runtime'

export const SLACK_API_ORIGIN = 'https://slack.com'
export const SLACK_INTEGRATION = 'slack'

export type SlackResponse = {
  ok: boolean
  error?: string
  needed?: string
  provided?: string
  response_metadata?: { next_cursor?: string }
  [key: string]: unknown
}

export class SlackApiError extends Error {
  readonly method: string
  readonly status: number
  readonly details: SlackResponse

  constructor(method: string, status: number, details: SlackResponse) {
    super('Slack API ' + method + ' failed: ' + (details.error || String(status)))
    this.name = 'SlackApiError'
    this.method = method
    this.status = status
    this.details = details
  }
}

let authenticatedFetch: typeof fetch | null = null

async function getAuthenticatedFetch(): Promise<typeof fetch> {
  if (!authenticatedFetch) {
    authenticatedFetch = await createAuthenticatedFetch(SLACK_INTEGRATION)
  }
  return authenticatedFetch
}

export async function slackRequest<T extends SlackResponse>(
  method: string,
  params: Record<string, unknown> = {},
): Promise<T> {
  if (!/^[a-z][a-z0-9_.]+$/.test(method)) {
    throw new Error('Invalid Slack Web API method name.')
  }

  const body = new URLSearchParams()
  for (const [key, value] of Object.entries(params)) {
    if (value === undefined || value === null || value === '') continue
    body.set(key, typeof value === 'object' ? JSON.stringify(value) : String(value))
  }

  const url = new URL('/api/' + method, SLACK_API_ORIGIN)
  const authedFetch = await getAuthenticatedFetch()
  const response = await authedFetch(url.toString(), {
    method: 'POST',
    headers: {
      accept: 'application/json',
      'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
    },
    body,
  })
  const result = (await response.json()) as SlackResponse

  if (!response.ok || result.ok !== true) {
    throw new SlackApiError(method, response.status, result)
  }

  return result as T
}

export function nextCursor(response: SlackResponse): string | null {
  const cursor = response.response_metadata?.next_cursor
  return typeof cursor === 'string' && cursor.length > 0 ? cursor : null
}