import { REDDIT_COMMENT_SCHEMA, REDDIT_CONTENT_ID_SCHEMA, REDDIT_ERROR_TUPLE_SCHEMA, REDDIT_JSON_STATUS_SCHEMA, REDDIT_POST_SCHEMA, REDDIT_PROVIDER_COMMENT_THING_SCHEMA, REDDIT_PROVIDER_CONTENT_THING_SCHEMA, REDDIT_SUBREDDIT_NAME_SCHEMA, REDDIT_THING_ID_SCHEMA, toRedditComment, toRedditPost, } from "@automate.ax/integration-contracts/reddit" import { z } from "zod" import { defineAction } from "../../automation/actions" import { getRedditApi, REDDIT_ACCOUNT, redditAccountRequirement } from "./lib" const REDDIT_POST_RESULT_SCHEMA = z.object({ id: z.string().regex(/^t3_[a-z0-9]+$/i), shortId: z.string(), url: z.url(), }) const REDDIT_SUBMIT_RESPONSE_SCHEMA = z.object({ json: z.object({ data: z.object({ id: z.string(), name: z.string().regex(/^t3_[a-z0-9]+$/i), url: z.url(), }), errors: REDDIT_ERROR_TUPLE_SCHEMA.array(), }), }) const REDDIT_COMMENT_RESPONSE_SCHEMA = z.object({ json: z.object({ data: z.object({ things: REDDIT_PROVIDER_COMMENT_THING_SCHEMA.array() }), errors: REDDIT_ERROR_TUPLE_SCHEMA.array(), }), }) const REDDIT_EDIT_RESPONSE_SCHEMA = z.object({ json: z.object({ data: z.object({ things: REDDIT_PROVIDER_CONTENT_THING_SCHEMA.array() }), errors: REDDIT_ERROR_TUPLE_SCHEMA.array(), }), }) const REDDIT_CREATED_POST_INPUT_FIELDS = { /** Optional post flair template. */ flairId: z.string().max(36).optional(), /** Mark the post as not safe for work. */ isNsfw: z.boolean().prefault(false), /** Notify the account about post replies. */ sendReplies: z.boolean().prefault(true), /** Mark the post as a spoiler. */ spoiler: z.boolean().prefault(false), subreddit: REDDIT_SUBREDDIT_NAME_SCHEMA, title: z.string().trim().min(1).max(300), } interface SubmitPostInput { flairId?: string isNsfw: boolean kind: "link" | "self" sendReplies: boolean spoiler: boolean subreddit: string text?: string title: string url?: string } interface StateActionOptions { label: string path: string replaySafety: "safe" | "unsafe" scope: string } /** Creates a self post with a Markdown body. */ export const createRedditTextPost = defineAction("Create Reddit text post") .account(REDDIT_ACCOUNT, redditAccountRequirement("submit")) .input( z.object({ ...REDDIT_CREATED_POST_INPUT_FIELDS, text: z.string().max(40_000).prefault(""), }), ) .output(REDDIT_POST_RESULT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => submitPost(account.secret, { ...input, kind: "self" }), ) /** Creates a link post. */ export const createRedditLinkPost = defineAction("Create Reddit link post") .account(REDDIT_ACCOUNT, redditAccountRequirement("submit")) .input( z.object({ ...REDDIT_CREATED_POST_INPUT_FIELDS, url: z.url(), }), ) .output(REDDIT_POST_RESULT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => submitPost(account.secret, { ...input, kind: "link" }), ) /** Creates a comment on a Reddit post or comment. */ export const createRedditComment = defineAction("Create Reddit comment") .account(REDDIT_ACCOUNT, redditAccountRequirement("submit")) .input( z.object({ parentId: REDDIT_CONTENT_ID_SCHEMA, text: z.string().min(1) }), ) .output(REDDIT_COMMENT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { // Keep the provider envelope named before validating its first created thing. const response = await getRedditApi(account.secret).request("api/comment", { body: { api_type: "json", text: input.text, thing_id: input.parentId, }, method: "POST", responseSchema: REDDIT_COMMENT_RESPONSE_SCHEMA, }) const comment = response.json.data.things[0] if (!comment) throw new Error("Reddit did not return the created comment.") return toRedditComment(comment.data) }) /** Edits the body of an owned self post or comment. */ export const editRedditContent = defineAction("Edit Reddit content") .account(REDDIT_ACCOUNT, redditAccountRequirement("edit")) .input(z.object({ contentId: REDDIT_CONTENT_ID_SCHEMA, text: z.string() })) .output(z.union([REDDIT_POST_SCHEMA, REDDIT_COMMENT_SCHEMA])) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { // Keep the provider envelope named before discriminating the edited thing. const response = await getRedditApi(account.secret).request( "api/editusertext", { body: { api_type: "json", text: input.text, thing_id: input.contentId, }, method: "POST", responseSchema: REDDIT_EDIT_RESPONSE_SCHEMA, }, ) const content = response.json.data.things[0] if (!content) throw new Error("Reddit did not return the edited content.") return content.kind === "t3" ? toRedditPost(content.data) : toRedditComment(content.data) }) /** Deletes an owned Reddit post or comment. */ export const deleteRedditContent = stateAction({ label: "Delete Reddit content", path: "api/del", replaySafety: "safe", scope: "edit", }) /** Saves a Reddit post or comment to the connected account. */ export const saveRedditContent = stateAction({ label: "Save Reddit content", path: "api/save", replaySafety: "safe", scope: "save", }) /** Removes a Reddit post or comment from the connected account's saved items. */ export const unsaveRedditContent = stateAction({ label: "Unsave Reddit content", path: "api/unsave", replaySafety: "safe", scope: "save", }) /** Reports a Reddit post, comment, or message and hides public content. */ export const reportRedditContent = defineAction("Report Reddit content") .account(REDDIT_ACCOUNT, redditAccountRequirement("report")) .input( z.object({ contentId: REDDIT_THING_ID_SCHEMA, customText: z.string().max(2_000).optional(), reason: z.string().trim().min(1).max(100), }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getRedditApi(account.secret).request("api/report", { body: { api_type: "json", custom_text: input.customText, reason: input.reason, thing_id: input.contentId, }, method: "POST", responseSchema: REDDIT_JSON_STATUS_SCHEMA, }) }) /** Submits a text or link post and returns its stable Reddit identifiers. */ /** * @param secret - Stored Reddit OAuth secret. * @param input - Normalized post submission. */ async function submitPost(secret: unknown, input: SubmitPostInput) { const response = await getRedditApi(secret).request("api/submit", { body: { api_type: "json", flair_id: input.flairId, kind: input.kind, nsfw: input.isNsfw, resubmit: true, sendreplies: input.sendReplies, spoiler: input.spoiler, sr: input.subreddit, text: input.text, title: input.title, url: input.url, }, method: "POST", responseSchema: REDDIT_SUBMIT_RESPONSE_SCHEMA, }) return REDDIT_POST_RESULT_SCHEMA.parse({ id: response.json.data.name, shortId: response.json.data.id, url: response.json.data.url, }) } /** Builds a one-ID Reddit state mutation. */ /** @param options - Action label, endpoint, scope, and replay behavior. */ function stateAction(options: StateActionOptions) { return defineAction(options.label) .account(REDDIT_ACCOUNT, redditAccountRequirement(options.scope)) .input(z.object({ contentId: REDDIT_CONTENT_ID_SCHEMA })) .output(z.void()) .retry({ replaySafety: options.replaySafety }) .handler(async ({ account, input }) => { await getRedditApi(account.secret).request(options.path, { body: { id: input.contentId }, method: "POST", responseSchema: z.undefined(), }) }) }