import { REDDIT_API_ORIGIN, REDDIT_CONNECT_URL, REDDIT_SCOPES, REDDIT_USER_AGENT, redditRequest, redditRequestWithResponse, resolveIntegration, } from './request.ts' import { listingPath, listingSort, normalizeSubreddit, normalizeThingId, normalizeUsername, optionalLimit, requireConfirm, requireString, searchSort, shapeCommentListing, shapeListing, shapeMe, shapeSubreddit, shapeUser, submitKind, timeFilter, userListing, voteDirection, } from './shape.ts' import type { GetListingInput, GetPostInput, GetSubredditInput, GetUserInput, JsonRecord, SearchInput, ShapedListing, SubmitCommentInput, SubmitPostInput, VoteInput, } from './types.ts' export type { AccountInput, GetListingInput, GetPostInput, GetSubredditInput, GetUserInput, JsonRecord, ListingSort, RedditRequestInput, RedditRequestResult, SearchInput, SearchSort, ShapedComment, ShapedListing, ShapedPost, ShapedSubreddit, ShapedUser, SubmitCommentInput, SubmitKind, SubmitPostInput, TimeFilter, UserListing, VoteDirection, VoteInput, } from './types.ts' export { RedditApiError, REDDIT_API_ORIGIN, REDDIT_AUTHORIZE_URL, REDDIT_CALLBACK_URL, REDDIT_CONNECT_URL, REDDIT_DASHBOARD_URL, REDDIT_INTEGRATION, REDDIT_SCOPES, REDDIT_TOKEN_URL, REDDIT_USER_AGENT, byoConnectUrl, redditRequest, request, resolveIntegration, } from './request.ts' export { requireRecord, requireStringField } from './types.ts' export function accounts(input: GetListingInput | Record = {}) { const integration = resolveIntegration(input) return { integration, account: input.account ?? null, convention: 'integration wins when set. account: "work" → reddit-work. account: "default" / "reddit" / omitted → reddit.', connectUrl: 'https://kody.codes/connect/oauth?provider=' + encodeURIComponent(integration), } } export async function getMe(input: GetListingInput | Record = {}) { const integration = resolveIntegration(input) const body = await redditRequest('/api/v1/me', { integration }) return { utility: 'get-me', integration, user: shapeMe(body), } } export async function smokeTest(input: GetListingInput | Record = {}) { const me = await getMe(input) return { ok: true, integration: me.integration, endpoint: REDDIT_API_ORIGIN + '/api/v1/me', tokenMode: 'saved OAuth integration', hasId: Boolean(me.user.id), hasName: Boolean(me.user.name), scopes: [...REDDIT_SCOPES], connectUrl: REDDIT_CONNECT_URL, userAgent: REDDIT_USER_AGENT, } } export async function getSubreddit(input: GetSubredditInput) { const integration = resolveIntegration(input) const subreddit = normalizeSubreddit(input.subreddit) const body = await redditRequest('/r/' + encodeURIComponent(subreddit) + '/about', { integration, }) return { utility: 'get-subreddit', integration, subreddit: shapeSubreddit(body), } } export async function getListing(input: GetListingInput = {}) { const integration = resolveIntegration(input) const sort = listingSort(input.sort) const subreddit = input.subreddit ? normalizeSubreddit(input.subreddit) : undefined const t = timeFilter(input.t) const query: Record = { limit: optionalLimit(input.limit), } if (t) query.t = t if (input.after) query.after = requireString(input.after, 'after must be a listing cursor.') if (input.before) query.before = requireString(input.before, 'before must be a listing cursor.') const path = listingPath(subreddit, sort) const body = await redditRequest(path, { integration, query }) return { utility: 'get-listing', integration, path, sort, subreddit: subreddit ?? null, listing: shapeListing(body), } } export async function searchReddit(input: SearchInput) { const integration = resolveIntegration(input) const queryText = requireString(input.query, 'search requires params.query.') const subreddit = input.subreddit ? normalizeSubreddit(input.subreddit) : undefined const sort = searchSort(input.sort) const t = timeFilter(input.t) const restrict = input.restrictToSubreddit === true || Boolean(subreddit && input.restrictToSubreddit !== false) const query: Record = { q: queryText, sort, limit: optionalLimit(input.limit), restrict_sr: Boolean(subreddit) && restrict, type: 'link', } if (t) query.t = t if (input.after) query.after = requireString(input.after, 'after must be a listing cursor.') const path = subreddit ? '/r/' + encodeURIComponent(subreddit) + '/search' : '/search' const body = await redditRequest(path, { integration, query }) return { utility: 'search', integration, path, query: queryText, subreddit: subreddit ?? null, sort, listing: shapeListing(body), } } export async function getUser(input: GetUserInput) { const integration = resolveIntegration(input) const username = normalizeUsername(input.username) const listing = userListing(input.listing) const base = '/user/' + encodeURIComponent(username) if (listing === 'about') { const body = await redditRequest(base + '/about', { integration }) return { utility: 'get-user', integration, listing, user: shapeUser(body), } } const sort = listingSort(input.sort, 'new') const t = timeFilter(input.t) const query: Record = { limit: optionalLimit(input.limit), sort, } if (t) query.t = t if (input.after) query.after = requireString(input.after, 'after must be a listing cursor.') const body = await redditRequest(base + '/' + listing, { integration, query }) return { utility: 'get-user', integration, listing, username, sort, listingData: shapeUserContent(body), } } export async function getPost(input: GetPostInput) { const integration = resolveIntegration(input) const id = normalizeThingId(input.id, 't3', 'params.id').slice(3) const subreddit = input.subreddit ? normalizeSubreddit(input.subreddit) : undefined const path = subreddit ? '/r/' + encodeURIComponent(subreddit) + '/comments/' + encodeURIComponent(id) : '/comments/' + encodeURIComponent(id) const query: Record = {} if (input.commentLimit !== undefined) query.limit = optionalLimit(input.commentLimit) if (input.commentSort) query.sort = String(input.commentSort) const body = await redditRequest(path, { integration, query }) if (!Array.isArray(body) || body.length < 1) { throw new Error('Reddit comments response did not include the post listing.') } const postListing = shapeListing(body[0]) const comments = body.length > 1 ? shapeCommentListing(body[1]) : [] return { utility: 'get-post', integration, path, post: postListing.posts[0] ?? null, comments, } } export async function submitPost(input: SubmitPostInput): Promise { const integration = resolveIntegration(input) const subreddit = normalizeSubreddit(input.subreddit) const title = requireString(input.title, 'submit-post requires params.title.') const kind = submitKind(input.kind, input.url ? 'link' : 'self') const form = new URLSearchParams({ api_type: 'json', kind, sr: subreddit, title, }) if (kind === 'link') { form.set('url', requireString(input.url, 'submit-post kind=link requires params.url.')) } else { form.set('text', input.text ?? '') } if (input.nsfw === true) form.set('nsfw', 'true') if (input.spoiler === true) form.set('spoiler', 'true') if (input.sendreplies !== undefined) form.set('sendreplies', input.sendreplies ? 'true' : 'false') if (input.flairId) form.set('flair_id', input.flairId) if (input.flairText) form.set('flair_text', input.flairText) if (input.dryRun) { return { dryRun: true, utility: 'submit-post', method: 'POST', url: REDDIT_API_ORIGIN + '/api/submit', integration, requiredScope: 'submit', payload: Object.fromEntries(form), requiresConfirm: true, publishNote: 'Pass params.confirm = true only after explicit user approval to submit this post.', } } requireConfirm( input.confirm, 'submit-post publishes to Reddit. Pass params.confirm = true only after explicit user approval.', ) const result = await redditRequestWithResponse('/api/submit', { method: 'POST', body: form, integration, }) return { utility: 'submit-post', integration, ok: result.ok, status: result.status, body: result.body, } } export async function submitComment(input: SubmitCommentInput): Promise { const integration = resolveIntegration(input) const parentId = normalizeParentId(input.parentId) const text = requireString(input.text, 'submit-comment requires params.text.') const form = new URLSearchParams({ api_type: 'json', thing_id: parentId, text, }) if (input.dryRun) { return { dryRun: true, utility: 'submit-comment', method: 'POST', url: REDDIT_API_ORIGIN + '/api/comment', integration, requiredScope: 'submit', payload: Object.fromEntries(form), requiresConfirm: true, publishNote: 'Pass params.confirm = true only after explicit user approval to submit this comment.', } } requireConfirm( input.confirm, 'submit-comment publishes to Reddit. Pass params.confirm = true only after explicit user approval.', ) const result = await redditRequestWithResponse('/api/comment', { method: 'POST', body: form, integration, }) return { utility: 'submit-comment', integration, ok: result.ok, status: result.status, body: result.body, } } export async function vote(input: VoteInput): Promise { const integration = resolveIntegration(input) const id = normalizeVoteId(input.id) const dir = voteDirection(input.direction) const form = new URLSearchParams({ id, dir: String(dir), }) if (input.dryRun) { return { dryRun: true, utility: 'vote', method: 'POST', url: REDDIT_API_ORIGIN + '/api/vote', integration, requiredScope: 'vote', payload: Object.fromEntries(form), requiresConfirm: true, publishNote: 'Pass params.confirm = true only after explicit user approval to cast this vote.', } } requireConfirm( input.confirm, 'vote changes a Reddit score. Pass params.confirm = true only after explicit user approval.', ) const result = await redditRequestWithResponse('/api/vote', { method: 'POST', body: form, integration, }) return { utility: 'vote', integration, ok: result.ok, status: result.status, id, direction: dir, body: result.body, } } function shapeUserContent(body: unknown): ShapedListing | { comments: ReturnType } { const record = body && typeof body === 'object' ? (body as JsonRecord) : null const kind = record && typeof record.kind === 'string' ? record.kind : null if (kind === 'Listing') { const children = Array.isArray((record?.data as JsonRecord | undefined)?.children) ? ((record?.data as JsonRecord).children as unknown[]) : [] const first = children[0] && typeof children[0] === 'object' ? (children[0] as JsonRecord) : null if (first?.kind === 't1') { return { comments: shapeCommentListing(body) } } } return shapeListing(body) } function normalizeParentId(value: unknown): string { const raw = requireString(value, 'submit-comment requires params.parentId.') if (raw.startsWith('t1_') || raw.startsWith('t3_')) return raw if (/^[A-Za-z0-9]+$/.test(raw)) return 't3_' + raw throw new Error('params.parentId must be a Reddit t3_ post or t1_ comment fullname.') } function normalizeVoteId(value: unknown): string { const raw = requireString(value, 'vote requires params.id.') if (raw.startsWith('t1_') || raw.startsWith('t3_')) return raw if (/^[A-Za-z0-9]+$/.test(raw)) return 't3_' + raw throw new Error('params.id must be a Reddit t3_ post or t1_ comment fullname.') }