import { REDDIT_COMMENT_SCHEMA, REDDIT_POST_PAGE_SCHEMA, REDDIT_POST_SCHEMA, REDDIT_PROVIDER_COMMENT_THING_SCHEMA, REDDIT_POST_ID_SCHEMA, REDDIT_PROVIDER_POST_LISTING_SCHEMA, REDDIT_PROVIDER_SUBREDDIT_THING_SCHEMA, REDDIT_SUBREDDIT_NAME_SCHEMA, REDDIT_SUBREDDIT_SCHEMA, REDDIT_THING_ID_SCHEMA, redditProviderListingSchema, toRedditComment, toRedditPageInfo, toRedditPost, toRedditSubreddit, } from "@automate.ax/integration-contracts/reddit" import { z } from "zod" import { defineAction } from "../../automation/actions" import { getRedditApi, REDDIT_ACCOUNT, redditAccountRequirement, redditPathSegment, } from "./lib" const REDDIT_LIST_INPUT_FIELDS = { /** Opaque fullname cursor returned by the previous page. */ after: REDDIT_THING_ID_SCHEMA.optional(), /** Maximum records to return. */ limit: z.number().int().min(1).max(100).prefault(25), } const REDDIT_POST_SORT_SCHEMA = z.enum([ "controversial", "hot", "new", "rising", "top", ]) const REDDIT_TIME_RANGE_SCHEMA = z.enum([ "all", "day", "hour", "month", "week", "year", ]) const REDDIT_COMMENT_SORT_SCHEMA = z.enum([ "confidence", "controversial", "live", "new", "old", "qa", "random", "top", ]) const REDDIT_POST_DETAILS_SCHEMA = z.object({ comments: REDDIT_COMMENT_SCHEMA.array(), post: REDDIT_POST_SCHEMA, }) const REDDIT_COMMENT_LISTING_SCHEMA = redditProviderListingSchema( z.union([ REDDIT_PROVIDER_COMMENT_THING_SCHEMA, z.object({ data: z.looseObject({}), kind: z.literal("more") }), ]), ) const REDDIT_COMMENT_TREE_RESPONSE_SCHEMA = z.tuple([ REDDIT_PROVIDER_POST_LISTING_SCHEMA, REDDIT_COMMENT_LISTING_SCHEMA, ]) /** Returns metadata for one subreddit. */ export const getRedditSubreddit = defineAction("Get Reddit subreddit") .account(REDDIT_ACCOUNT, redditAccountRequirement("read")) .input(z.object({ subreddit: REDDIT_SUBREDDIT_NAME_SCHEMA })) .output(REDDIT_SUBREDDIT_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { // Keep the provider thing named before unwrapping its data. const response = await getRedditApi(account.secret).request( `r/${redditPathSegment(input.subreddit)}/about`, { responseSchema: REDDIT_PROVIDER_SUBREDDIT_THING_SCHEMA }, ) return toRedditSubreddit(response.data) }) /** Lists posts from one subreddit using Reddit's native ranking modes. */ export const listRedditPosts = defineAction("List Reddit posts") .account(REDDIT_ACCOUNT, redditAccountRequirement("read")) .input( z.object({ ...REDDIT_LIST_INPUT_FIELDS, sort: REDDIT_POST_SORT_SCHEMA.prefault("new"), subreddit: REDDIT_SUBREDDIT_NAME_SCHEMA, /** Time window used by top and controversial sorts. */ time: REDDIT_TIME_RANGE_SCHEMA.optional(), }), ) .output(REDDIT_POST_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const response = await getRedditApi(account.secret).request( `r/${redditPathSegment(input.subreddit)}/${input.sort}`, { query: { after: input.after, limit: input.limit, ...(input.time && { t: input.time }), }, responseSchema: REDDIT_PROVIDER_POST_LISTING_SCHEMA, }, ) return { pageInfo: toRedditPageInfo(response), posts: response.data.children.map(({ data }) => toRedditPost(data)), } }) /** Searches Reddit posts globally or within one subreddit. */ export const searchRedditPosts = defineAction("Search Reddit posts") .account(REDDIT_ACCOUNT, redditAccountRequirement("read")) .input( z.object({ ...REDDIT_LIST_INPUT_FIELDS, query: z.string().trim().min(1).max(512), sort: z .enum(["comments", "hot", "new", "relevance", "top"]) .prefault("relevance"), subreddit: REDDIT_SUBREDDIT_NAME_SCHEMA.optional(), time: REDDIT_TIME_RANGE_SCHEMA.prefault("all"), }), ) .output(REDDIT_POST_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { // Name the optional subreddit path once before the shared request mapping. const path = input.subreddit ? `r/${redditPathSegment(input.subreddit)}/search` : "search" const response = await getRedditApi(account.secret).request(path, { query: { after: input.after, limit: input.limit, q: input.query, restrict_sr: Boolean(input.subreddit), sort: input.sort, t: input.time, }, responseSchema: REDDIT_PROVIDER_POST_LISTING_SCHEMA, }) return { pageInfo: toRedditPageInfo(response), posts: response.data.children.map(({ data }) => toRedditPost(data)), } }) /** Retrieves one Reddit post by fullname. */ export const getRedditPost = defineAction("Get Reddit post") .account(REDDIT_ACCOUNT, redditAccountRequirement("read")) .input(z.object({ postId: REDDIT_POST_ID_SCHEMA })) .output(REDDIT_POST_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { // Keep the listing named before requiring its first post. const response = await getRedditApi(account.secret).request("api/info", { query: { id: input.postId }, responseSchema: REDDIT_PROVIDER_POST_LISTING_SCHEMA, }) const post = response.data.children[0] if (!post) throw new Error(`Reddit post was not returned: ${input.postId}`) return toRedditPost(post.data) }) /** Retrieves a Reddit post and its available comment tree. */ export const listRedditComments = defineAction("List Reddit comments") .account(REDDIT_ACCOUNT, redditAccountRequirement("read")) .input( z.object({ /** Maximum nested comment depth Reddit should return. */ depth: z.number().int().min(1).max(10).optional(), /** Maximum comments Reddit should return. */ limit: z.number().int().min(1).max(500).prefault(100), postId: REDDIT_POST_ID_SCHEMA, sort: REDDIT_COMMENT_SORT_SCHEMA.prefault("confidence"), }), ) .output(REDDIT_POST_DETAILS_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const [posts, comments] = await getRedditApi(account.secret).request( `comments/${redditPathSegment(input.postId.slice(3))}`, { query: { depth: input.depth, limit: input.limit, sort: input.sort, }, responseSchema: REDDIT_COMMENT_TREE_RESPONSE_SCHEMA, }, ) const post = posts.data.children[0] if (!post) throw new Error(`Reddit post was not returned: ${input.postId}`) return { comments: flattenComments(comments.data.children), post: toRedditPost(post.data), } }) /** Flattens Reddit's nested comment listings in display order. */ /** @param things - Provider comment listing children. */ function flattenComments( things: z.output["data"]["children"], ): z.output[] { return things.flatMap((thing) => { if (thing.kind !== "t1") return [] const replies = REDDIT_COMMENT_LISTING_SCHEMA.safeParse(thing.data.replies) return [ toRedditComment(thing.data), ...(replies.success ? flattenComments(replies.data.data.children) : []), ] }) }