From ecd3130b07d3e024bb0982fb7d6c71ead0a2f216 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 19:25:21 -0800 Subject: [PATCH 1/5] feat(reddit): add 5 new tools, fix bugs, and audit all endpoints against API docs --- apps/sim/blocks/blocks/reddit.ts | 666 +++++++++++------- .../migrations/subblock-migrations.test.ts | 24 +- apps/sim/tools/reddit/delete.ts | 4 +- apps/sim/tools/reddit/get_comments.ts | 50 +- apps/sim/tools/reddit/get_controversial.ts | 34 +- apps/sim/tools/reddit/get_me.ts | 72 ++ apps/sim/tools/reddit/get_messages.ts | 166 +++++ apps/sim/tools/reddit/get_posts.ts | 54 +- apps/sim/tools/reddit/get_subreddit_info.ts | 102 +++ apps/sim/tools/reddit/get_user.ts | 82 +++ apps/sim/tools/reddit/hot_posts.ts | 55 +- apps/sim/tools/reddit/index.ts | 10 + apps/sim/tools/reddit/reply.ts | 9 + apps/sim/tools/reddit/search.ts | 49 +- apps/sim/tools/reddit/send_message.ts | 111 +++ apps/sim/tools/reddit/submit_post.ts | 25 +- apps/sim/tools/reddit/types.ts | 236 ++++++- apps/sim/tools/registry.ts | 10 + 18 files changed, 1385 insertions(+), 374 deletions(-) create mode 100644 apps/sim/tools/reddit/get_me.ts create mode 100644 apps/sim/tools/reddit/get_messages.ts create mode 100644 apps/sim/tools/reddit/get_subreddit_info.ts create mode 100644 apps/sim/tools/reddit/get_user.ts create mode 100644 apps/sim/tools/reddit/send_message.ts diff --git a/apps/sim/blocks/blocks/reddit.ts b/apps/sim/blocks/blocks/reddit.ts index 828d4e338eb..83c0aa6761c 100644 --- a/apps/sim/blocks/blocks/reddit.ts +++ b/apps/sim/blocks/blocks/reddit.ts @@ -9,13 +9,12 @@ export const RedditBlock: BlockConfig = { description: 'Access Reddit data and content', authMode: AuthMode.OAuth, longDescription: - 'Integrate Reddit into workflows. Read posts, comments, and search content. Submit posts, vote, reply, edit, and manage your Reddit account.', + 'Integrate Reddit into workflows. Read posts, comments, and search content. Submit posts, vote, reply, edit, manage messages, and access user and subreddit info.', docsLink: 'https://docs.sim.ai/tools/reddit', category: 'tools', bgColor: '#FF5700', icon: RedditIcon, subBlocks: [ - // Operation selection { id: 'operation', title: 'Operation', @@ -33,6 +32,11 @@ export const RedditBlock: BlockConfig = { { label: 'Edit', id: 'edit' }, { label: 'Delete', id: 'delete' }, { label: 'Subscribe', id: 'subscribe' }, + { label: 'Get My Profile', id: 'get_me' }, + { label: 'Get User Profile', id: 'get_user' }, + { label: 'Send Message', id: 'send_message' }, + { label: 'Get Messages', id: 'get_messages' }, + { label: 'Get Subreddit Info', id: 'get_subreddit_info' }, ], value: () => 'get_posts', }, @@ -76,7 +80,7 @@ export const RedditBlock: BlockConfig = { required: true, }, - // Common fields - appear for all actions + // ── Get Posts ────────────────────────────────────────────────────── { id: 'subreddit', title: 'Subreddit', @@ -84,12 +88,10 @@ export const RedditBlock: BlockConfig = { placeholder: 'Enter subreddit name (without r/)', condition: { field: 'operation', - value: ['get_posts', 'get_comments', 'get_controversial', 'search'], + value: ['get_posts', 'get_comments', 'get_controversial', 'search', 'get_subreddit_info'], }, required: true, }, - - // Get Posts specific fields { id: 'sort', title: 'Sort By', @@ -99,18 +101,17 @@ export const RedditBlock: BlockConfig = { { label: 'New', id: 'new' }, { label: 'Top', id: 'top' }, { label: 'Rising', id: 'rising' }, + { label: 'Controversial', id: 'controversial' }, ], - condition: { - field: 'operation', - value: 'get_posts', - }, + condition: { field: 'operation', value: 'get_posts' }, required: true, }, { id: 'time', - title: 'Time Filter (for Top sort)', + title: 'Time Filter', type: 'dropdown', options: [ + { label: 'Hour', id: 'hour' }, { label: 'Day', id: 'day' }, { label: 'Week', id: 'week' }, { label: 'Month', id: 'month' }, @@ -120,10 +121,7 @@ export const RedditBlock: BlockConfig = { condition: { field: 'operation', value: 'get_posts', - and: { - field: 'sort', - value: 'top', - }, + and: { field: 'sort', value: ['top', 'controversial'] }, }, }, { @@ -131,22 +129,38 @@ export const RedditBlock: BlockConfig = { title: 'Max Posts', type: 'short-input', placeholder: '10', + condition: { field: 'operation', value: 'get_posts' }, + }, + { + id: 'after', + title: 'After', + type: 'short-input', + placeholder: 'Fullname for forward pagination (e.g., t3_xxxxx)', condition: { field: 'operation', - value: 'get_posts', + value: ['get_posts', 'get_controversial', 'search'], }, + mode: 'advanced', }, - - // Get Comments specific fields { - id: 'postId', - title: 'Post ID', + id: 'before', + title: 'Before', type: 'short-input', - placeholder: 'Enter post ID', + placeholder: 'Fullname for backward pagination (e.g., t3_xxxxx)', condition: { field: 'operation', - value: 'get_comments', + value: ['get_posts', 'get_controversial', 'search'], }, + mode: 'advanced', + }, + + // ── Get Comments ────────────────────────────────────────────────── + { + id: 'postId', + title: 'Post ID', + type: 'short-input', + placeholder: 'Enter post ID (e.g., abc123)', + condition: { field: 'operation', value: 'get_comments' }, required: true, }, { @@ -154,7 +168,7 @@ export const RedditBlock: BlockConfig = { title: 'Sort Comments By', type: 'dropdown', options: [ - { label: 'Confidence', id: 'confidence' }, + { label: 'Best', id: 'confidence' }, { label: 'Top', id: 'top' }, { label: 'New', id: 'new' }, { label: 'Controversial', id: 'controversial' }, @@ -162,23 +176,33 @@ export const RedditBlock: BlockConfig = { { label: 'Random', id: 'random' }, { label: 'Q&A', id: 'qa' }, ], - condition: { - field: 'operation', - value: 'get_comments', - }, + condition: { field: 'operation', value: 'get_comments' }, }, { id: 'commentLimit', title: 'Number of Comments', type: 'short-input', placeholder: '50', - condition: { - field: 'operation', - value: 'get_comments', - }, + condition: { field: 'operation', value: 'get_comments' }, + }, + { + id: 'commentDepth', + title: 'Max Reply Depth', + type: 'short-input', + placeholder: 'Max depth of nested replies', + condition: { field: 'operation', value: 'get_comments' }, + mode: 'advanced', + }, + { + id: 'commentFocus', + title: 'Focus Comment ID', + type: 'short-input', + placeholder: 'ID36 of a specific comment to focus on', + condition: { field: 'operation', value: 'get_comments' }, + mode: 'advanced', }, - // Get Controversial specific fields + // ── Get Controversial ───────────────────────────────────────────── { id: 'controversialTime', title: 'Time Filter', @@ -191,33 +215,44 @@ export const RedditBlock: BlockConfig = { { label: 'Year', id: 'year' }, { label: 'All Time', id: 'all' }, ], - condition: { - field: 'operation', - value: 'get_controversial', - }, + condition: { field: 'operation', value: 'get_controversial' }, }, { id: 'controversialLimit', title: 'Max Posts', type: 'short-input', placeholder: '10', - condition: { - field: 'operation', - value: 'get_controversial', - }, + condition: { field: 'operation', value: 'get_controversial' }, }, - // Search specific fields + // ── Search ──────────────────────────────────────────────────────── { id: 'searchQuery', title: 'Search Query', type: 'short-input', placeholder: 'Enter search query', - condition: { - field: 'operation', - value: 'search', - }, + condition: { field: 'operation', value: 'search' }, required: true, + wandConfig: { + enabled: true, + prompt: `Generate a Reddit search query based on the user's description. + +Reddit search supports: +- Simple text: "machine learning" +- Field searches: title:question, author:username, selftext:content, subreddit:name, url:example.com, site:example.com, flair:discussion +- Boolean operators: AND, OR, NOT (must be uppercase) +- Grouping with parentheses: (cats OR dogs) AND cute +- Exact phrases with quotes: "exact phrase" + +Examples: +- "posts about AI from last month" -> artificial intelligence +- "questions about Python" -> title:question python +- "posts linking to github" -> site:github.com +- "posts by user spez" -> author:spez + +Return ONLY the search query - no explanations, no extra text.`, + placeholder: 'Describe what you want to search for...', + }, }, { id: 'searchSort', @@ -230,10 +265,7 @@ export const RedditBlock: BlockConfig = { { label: 'New', id: 'new' }, { label: 'Comments', id: 'comments' }, ], - condition: { - field: 'operation', - value: 'search', - }, + condition: { field: 'operation', value: 'search' }, }, { id: 'searchTime', @@ -247,43 +279,30 @@ export const RedditBlock: BlockConfig = { { label: 'Year', id: 'year' }, { label: 'All Time', id: 'all' }, ], - condition: { - field: 'operation', - value: 'search', - }, + condition: { field: 'operation', value: 'search' }, }, { id: 'searchLimit', title: 'Max Results', type: 'short-input', placeholder: '10', - condition: { - field: 'operation', - value: 'search', - }, + condition: { field: 'operation', value: 'search' }, }, - - // Submit Post specific fields + // ── Submit Post ─────────────────────────────────────────────────── { id: 'submitSubreddit', title: 'Subreddit', type: 'short-input', placeholder: 'Enter subreddit name (without r/)', - condition: { - field: 'operation', - value: 'submit_post', - }, + condition: { field: 'operation', value: 'submit_post' }, required: true, }, { id: 'title', title: 'Post Title', type: 'short-input', - placeholder: 'Enter post title', - condition: { - field: 'operation', - value: 'submit_post', - }, + placeholder: 'Enter post title (max 300 characters)', + condition: { field: 'operation', value: 'submit_post' }, required: true, }, { @@ -294,10 +313,7 @@ export const RedditBlock: BlockConfig = { { label: 'Text Post', id: 'text' }, { label: 'Link Post', id: 'link' }, ], - condition: { - field: 'operation', - value: 'submit_post', - }, + condition: { field: 'operation', value: 'submit_post' }, value: () => 'text', required: true, }, @@ -309,10 +325,27 @@ export const RedditBlock: BlockConfig = { condition: { field: 'operation', value: 'submit_post', - and: { - field: 'postType', - value: 'text', - }, + and: { field: 'postType', value: 'text' }, + }, + wandConfig: { + enabled: true, + maintainHistory: true, + prompt: `Generate Reddit post content in markdown format based on the user's description. + +Reddit markdown supports: +- **bold**, *italic*, ~~strikethrough~~ +- [links](url), ![images](url) +- > blockquotes +- - bullet lists, 1. numbered lists +- \`inline code\`, code blocks with triple backticks +- Headers with # (use sparingly) +- Horizontal rules with --- +- Tables with | pipes | +- Superscript with ^ + +Write engaging, well-formatted content appropriate for the subreddit context. +Return ONLY the markdown content - no meta-commentary.`, + placeholder: 'Describe what your post should say...', }, }, { @@ -323,10 +356,7 @@ export const RedditBlock: BlockConfig = { condition: { field: 'operation', value: 'submit_post', - and: { - field: 'postType', - value: 'link', - }, + and: { field: 'postType', value: 'link' }, }, }, { @@ -337,10 +367,7 @@ export const RedditBlock: BlockConfig = { { label: 'No', id: 'false' }, { label: 'Yes', id: 'true' }, ], - condition: { - field: 'operation', - value: 'submit_post', - }, + condition: { field: 'operation', value: 'submit_post' }, value: () => 'false', }, { @@ -351,23 +378,45 @@ export const RedditBlock: BlockConfig = { { label: 'No', id: 'false' }, { label: 'Yes', id: 'true' }, ], - condition: { - field: 'operation', - value: 'submit_post', - }, + condition: { field: 'operation', value: 'submit_post' }, value: () => 'false', }, + { + id: 'flairId', + title: 'Flair ID', + type: 'short-input', + placeholder: 'Flair template ID (max 36 characters)', + condition: { field: 'operation', value: 'submit_post' }, + mode: 'advanced', + }, + { + id: 'flairText', + title: 'Flair Text', + type: 'short-input', + placeholder: 'Flair text to display (max 64 characters)', + condition: { field: 'operation', value: 'submit_post' }, + mode: 'advanced', + }, + { + id: 'sendReplies', + title: 'Send Reply Notifications', + type: 'dropdown', + options: [ + { label: 'Yes (default)', id: 'true' }, + { label: 'No', id: 'false' }, + ], + condition: { field: 'operation', value: 'submit_post' }, + mode: 'advanced', + value: () => 'true', + }, - // Vote specific fields + // ── Vote ────────────────────────────────────────────────────────── { id: 'voteId', title: 'Post/Comment ID', type: 'short-input', - placeholder: 'Enter thing ID (e.g., t3_xxxxx for post, t1_xxxxx for comment)', - condition: { - field: 'operation', - value: 'vote', - }, + placeholder: 'Thing fullname (e.g., t3_xxxxx for post, t1_xxxxx for comment)', + condition: { field: 'operation', value: 'vote' }, required: true, }, { @@ -379,47 +428,36 @@ export const RedditBlock: BlockConfig = { { label: 'Unvote', id: '0' }, { label: 'Downvote', id: '-1' }, ], - condition: { - field: 'operation', - value: 'vote', - }, + condition: { field: 'operation', value: 'vote' }, value: () => '1', required: true, }, - // Save/Unsave specific fields + // ── Save / Unsave ───────────────────────────────────────────────── { id: 'saveId', title: 'Post/Comment ID', type: 'short-input', - placeholder: 'Enter thing ID (e.g., t3_xxxxx for post, t1_xxxxx for comment)', - condition: { - field: 'operation', - value: ['save', 'unsave'], - }, + placeholder: 'Thing fullname (e.g., t3_xxxxx for post, t1_xxxxx for comment)', + condition: { field: 'operation', value: ['save', 'unsave'] }, required: true, }, { id: 'saveCategory', title: 'Category', type: 'short-input', - placeholder: 'Enter category name', - condition: { - field: 'operation', - value: 'save', - }, + placeholder: 'Category name (Reddit Premium feature)', + condition: { field: 'operation', value: 'save' }, + mode: 'advanced', }, - // Reply specific fields + // ── Reply ───────────────────────────────────────────────────────── { id: 'replyParentId', title: 'Parent Post/Comment ID', type: 'short-input', - placeholder: 'Enter thing ID to reply to (e.g., t3_xxxxx for post, t1_xxxxx for comment)', - condition: { - field: 'operation', - value: 'reply', - }, + placeholder: 'Thing fullname to reply to (e.g., t3_xxxxx or t1_xxxxx)', + condition: { field: 'operation', value: 'reply' }, required: true, }, { @@ -427,23 +465,33 @@ export const RedditBlock: BlockConfig = { title: 'Reply Text (Markdown)', type: 'long-input', placeholder: 'Enter reply text in markdown format', - condition: { - field: 'operation', - value: 'reply', - }, + condition: { field: 'operation', value: 'reply' }, required: true, + wandConfig: { + enabled: true, + maintainHistory: true, + prompt: `Generate a Reddit comment reply in markdown format based on the user's description. + +Reddit markdown supports: +- **bold**, *italic*, ~~strikethrough~~ +- [links](url) +- > blockquotes (for quoting parent) +- - bullet lists, 1. numbered lists +- \`inline code\`, code blocks with triple backticks + +Write a natural, conversational reply. Match the tone to the context. +Return ONLY the markdown content - no meta-commentary.`, + placeholder: 'Describe what your reply should say...', + }, }, - // Edit specific fields + // ── Edit ────────────────────────────────────────────────────────── { id: 'editThingId', title: 'Post/Comment ID', type: 'short-input', - placeholder: 'Enter thing ID to edit (e.g., t3_xxxxx for post, t1_xxxxx for comment)', - condition: { - field: 'operation', - value: 'edit', - }, + placeholder: 'Thing fullname to edit (e.g., t3_xxxxx or t1_xxxxx)', + condition: { field: 'operation', value: 'edit' }, required: true, }, { @@ -451,36 +499,27 @@ export const RedditBlock: BlockConfig = { title: 'New Text (Markdown)', type: 'long-input', placeholder: 'Enter new text in markdown format', - condition: { - field: 'operation', - value: 'edit', - }, + condition: { field: 'operation', value: 'edit' }, required: true, }, - // Delete specific fields + // ── Delete ──────────────────────────────────────────────────────── { id: 'deleteId', title: 'Post/Comment ID', type: 'short-input', - placeholder: 'Enter thing ID to delete (e.g., t3_xxxxx for post, t1_xxxxx for comment)', - condition: { - field: 'operation', - value: 'delete', - }, + placeholder: 'Thing fullname to delete (e.g., t3_xxxxx or t1_xxxxx)', + condition: { field: 'operation', value: 'delete' }, required: true, }, - // Subscribe specific fields + // ── Subscribe ───────────────────────────────────────────────────── { id: 'subscribeSubreddit', title: 'Subreddit', type: 'short-input', placeholder: 'Enter subreddit name (without r/)', - condition: { - field: 'operation', - value: 'subscribe', - }, + condition: { field: 'operation', value: 'subscribe' }, required: true, }, { @@ -491,13 +530,98 @@ export const RedditBlock: BlockConfig = { { label: 'Subscribe', id: 'sub' }, { label: 'Unsubscribe', id: 'unsub' }, ], - condition: { - field: 'operation', - value: 'subscribe', - }, + condition: { field: 'operation', value: 'subscribe' }, value: () => 'sub', required: true, }, + + // ── Get User Profile ────────────────────────────────────────────── + { + id: 'username', + title: 'Username', + type: 'short-input', + placeholder: 'Reddit username (e.g., spez)', + condition: { field: 'operation', value: 'get_user' }, + required: true, + }, + + // ── Send Message ────────────────────────────────────────────────── + { + id: 'messageTo', + title: 'Recipient', + type: 'short-input', + placeholder: 'Username or /r/subreddit', + condition: { field: 'operation', value: 'send_message' }, + required: true, + }, + { + id: 'messageSubject', + title: 'Subject', + type: 'short-input', + placeholder: 'Message subject (max 100 characters)', + condition: { field: 'operation', value: 'send_message' }, + required: true, + }, + { + id: 'messageText', + title: 'Message Body (Markdown)', + type: 'long-input', + placeholder: 'Enter message in markdown format', + condition: { field: 'operation', value: 'send_message' }, + required: true, + wandConfig: { + enabled: true, + prompt: `Generate a Reddit private message in markdown format based on the user's description. + +Write a clear, polite message. Reddit markdown supports **bold**, *italic*, [links](url), > quotes, lists, and code blocks. +Return ONLY the message content - no meta-commentary.`, + placeholder: 'Describe what your message should say...', + }, + }, + { + id: 'messageFromSr', + title: 'Send From Subreddit', + type: 'short-input', + placeholder: 'Subreddit name (requires mod mail permission)', + condition: { field: 'operation', value: 'send_message' }, + mode: 'advanced', + }, + + // ── Get Messages ────────────────────────────────────────────────── + { + id: 'messageWhere', + title: 'Message Folder', + type: 'dropdown', + options: [ + { label: 'Inbox (all)', id: 'inbox' }, + { label: 'Unread', id: 'unread' }, + { label: 'Sent', id: 'sent' }, + { label: 'Direct Messages', id: 'messages' }, + { label: 'Comment Replies', id: 'comments' }, + { label: 'Self-Post Replies', id: 'selfreply' }, + { label: 'Username Mentions', id: 'mentions' }, + ], + condition: { field: 'operation', value: 'get_messages' }, + value: () => 'inbox', + }, + { + id: 'messageLimit', + title: 'Max Messages', + type: 'short-input', + placeholder: '25', + condition: { field: 'operation', value: 'get_messages' }, + }, + { + id: 'messageMark', + title: 'Mark as Read', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + condition: { field: 'operation', value: 'get_messages' }, + mode: 'advanced', + }, ], tools: { access: [ @@ -513,163 +637,203 @@ export const RedditBlock: BlockConfig = { 'reddit_edit', 'reddit_delete', 'reddit_subscribe', + 'reddit_get_me', + 'reddit_get_user', + 'reddit_send_message', + 'reddit_get_messages', + 'reddit_get_subreddit_info', ], config: { tool: (inputs) => { const operation = inputs.operation || 'get_posts' - - if (operation === 'get_comments') { - return 'reddit_get_comments' + const toolMap: Record = { + get_posts: 'reddit_get_posts', + get_comments: 'reddit_get_comments', + get_controversial: 'reddit_get_controversial', + search: 'reddit_search', + submit_post: 'reddit_submit_post', + vote: 'reddit_vote', + save: 'reddit_save', + unsave: 'reddit_unsave', + reply: 'reddit_reply', + edit: 'reddit_edit', + delete: 'reddit_delete', + subscribe: 'reddit_subscribe', + get_me: 'reddit_get_me', + get_user: 'reddit_get_user', + send_message: 'reddit_send_message', + get_messages: 'reddit_get_messages', + get_subreddit_info: 'reddit_get_subreddit_info', } - - if (operation === 'get_controversial') { - return 'reddit_get_controversial' - } - - if (operation === 'search') { - return 'reddit_search' - } - - if (operation === 'submit_post') { - return 'reddit_submit_post' - } - - if (operation === 'vote') { - return 'reddit_vote' - } - - if (operation === 'save') { - return 'reddit_save' - } - - if (operation === 'unsave') { - return 'reddit_unsave' - } - - if (operation === 'reply') { - return 'reddit_reply' - } - - if (operation === 'edit') { - return 'reddit_edit' - } - - if (operation === 'delete') { - return 'reddit_delete' - } - - if (operation === 'subscribe') { - return 'reddit_subscribe' - } - - return 'reddit_get_posts' + return toolMap[operation] || 'reddit_get_posts' }, params: (inputs) => { const operation = inputs.operation || 'get_posts' - const { oauthCredential, ...rest } = inputs + const { oauthCredential } = inputs + + if (operation === 'get_posts') { + return { + subreddit: inputs.subreddit, + sort: inputs.sort, + time: + inputs.sort === 'top' || inputs.sort === 'controversial' ? inputs.time : undefined, + limit: inputs.limit ? Number.parseInt(inputs.limit) : undefined, + after: inputs.after || undefined, + before: inputs.before || undefined, + oauthCredential, + } + } if (operation === 'get_comments') { return { - postId: rest.postId, - subreddit: rest.subreddit, - sort: rest.commentSort, - limit: rest.commentLimit ? Number.parseInt(rest.commentLimit) : undefined, - oauthCredential: oauthCredential, + postId: inputs.postId, + subreddit: inputs.subreddit, + sort: inputs.commentSort, + limit: inputs.commentLimit ? Number.parseInt(inputs.commentLimit) : undefined, + depth: inputs.commentDepth ? Number.parseInt(inputs.commentDepth) : undefined, + comment: inputs.commentFocus || undefined, + oauthCredential, } } if (operation === 'get_controversial') { return { - subreddit: rest.subreddit, - time: rest.controversialTime, - limit: rest.controversialLimit ? Number.parseInt(rest.controversialLimit) : undefined, - oauthCredential: oauthCredential, + subreddit: inputs.subreddit, + time: inputs.controversialTime, + limit: inputs.controversialLimit + ? Number.parseInt(inputs.controversialLimit) + : undefined, + after: inputs.after || undefined, + before: inputs.before || undefined, + oauthCredential, } } if (operation === 'search') { return { - subreddit: rest.subreddit, - query: rest.searchQuery, - sort: rest.searchSort, - time: rest.searchTime, - limit: rest.searchLimit ? Number.parseInt(rest.searchLimit) : undefined, - oauthCredential: oauthCredential, + subreddit: inputs.subreddit, + query: inputs.searchQuery, + sort: inputs.searchSort, + time: inputs.searchTime, + limit: inputs.searchLimit ? Number.parseInt(inputs.searchLimit) : undefined, + after: inputs.after || undefined, + before: inputs.before || undefined, + oauthCredential, } } if (operation === 'submit_post') { return { - subreddit: rest.submitSubreddit, - title: rest.title, - text: rest.postType === 'text' ? rest.text : undefined, - url: rest.postType === 'link' ? rest.url : undefined, - nsfw: rest.nsfw === 'true', - spoiler: rest.spoiler === 'true', - oauthCredential: oauthCredential, + subreddit: inputs.submitSubreddit, + title: inputs.title, + text: inputs.postType === 'text' ? inputs.text : undefined, + url: inputs.postType === 'link' ? inputs.url : undefined, + nsfw: inputs.nsfw === 'true', + spoiler: inputs.spoiler === 'true', + send_replies: + inputs.sendReplies !== undefined ? inputs.sendReplies === 'true' : undefined, + flair_id: inputs.flairId || undefined, + flair_text: inputs.flairText || undefined, + oauthCredential, } } if (operation === 'vote') { return { - id: rest.voteId, - dir: Number.parseInt(rest.voteDirection), - oauthCredential: oauthCredential, + id: inputs.voteId, + dir: Number.parseInt(inputs.voteDirection), + oauthCredential, } } if (operation === 'save') { return { - id: rest.saveId, - category: rest.saveCategory, - oauthCredential: oauthCredential, + id: inputs.saveId, + category: inputs.saveCategory || undefined, + oauthCredential, } } if (operation === 'unsave') { return { - id: rest.saveId, - oauthCredential: oauthCredential, + id: inputs.saveId, + oauthCredential, } } if (operation === 'reply') { return { - parent_id: rest.replyParentId, - text: rest.replyText, - oauthCredential: oauthCredential, + parent_id: inputs.replyParentId, + text: inputs.replyText, + oauthCredential, } } if (operation === 'edit') { return { - thing_id: rest.editThingId, - text: rest.editText, - oauthCredential: oauthCredential, + thing_id: inputs.editThingId, + text: inputs.editText, + oauthCredential, } } if (operation === 'delete') { return { - id: rest.deleteId, - oauthCredential: oauthCredential, + id: inputs.deleteId, + oauthCredential, } } if (operation === 'subscribe') { return { - subreddit: rest.subscribeSubreddit, - action: rest.subscribeAction, - oauthCredential: oauthCredential, + subreddit: inputs.subscribeSubreddit, + action: inputs.subscribeAction, + oauthCredential, + } + } + + if (operation === 'get_me') { + return { oauthCredential } + } + + if (operation === 'get_user') { + return { + username: inputs.username, + oauthCredential, + } + } + + if (operation === 'send_message') { + return { + to: inputs.messageTo, + subject: inputs.messageSubject, + text: inputs.messageText, + from_sr: inputs.messageFromSr || undefined, + oauthCredential, + } + } + + if (operation === 'get_messages') { + return { + where: inputs.messageWhere, + limit: inputs.messageLimit ? Number.parseInt(inputs.messageLimit) : undefined, + mark: inputs.messageMark !== undefined ? inputs.messageMark === 'true' : undefined, + oauthCredential, + } + } + + if (operation === 'get_subreddit_info') { + return { + subreddit: inputs.subreddit, + oauthCredential, } } return { - subreddit: rest.subreddit, - sort: rest.sort, - limit: rest.limit ? Number.parseInt(rest.limit) : undefined, - time: rest.sort === 'top' ? rest.time : undefined, - oauthCredential: oauthCredential, + subreddit: inputs.subreddit, + sort: inputs.sort, + limit: inputs.limit ? Number.parseInt(inputs.limit) : undefined, + oauthCredential, } }, }, @@ -681,9 +845,13 @@ export const RedditBlock: BlockConfig = { sort: { type: 'string', description: 'Sort order' }, time: { type: 'string', description: 'Time filter' }, limit: { type: 'number', description: 'Maximum posts' }, + after: { type: 'string', description: 'Pagination cursor (after)' }, + before: { type: 'string', description: 'Pagination cursor (before)' }, postId: { type: 'string', description: 'Post identifier' }, commentSort: { type: 'string', description: 'Comment sort order' }, commentLimit: { type: 'number', description: 'Maximum comments' }, + commentDepth: { type: 'number', description: 'Maximum reply depth' }, + commentFocus: { type: 'string', description: 'Focus on specific comment' }, controversialTime: { type: 'string', description: 'Time filter for controversial posts' }, controversialLimit: { type: 'number', description: 'Maximum controversial posts' }, searchQuery: { type: 'string', description: 'Search query text' }, @@ -697,6 +865,9 @@ export const RedditBlock: BlockConfig = { url: { type: 'string', description: 'URL for link posts' }, nsfw: { type: 'boolean', description: 'Mark post as NSFW' }, spoiler: { type: 'boolean', description: 'Mark post as spoiler' }, + sendReplies: { type: 'boolean', description: 'Send reply notifications' }, + flairId: { type: 'string', description: 'Flair template ID' }, + flairText: { type: 'string', description: 'Flair display text' }, voteId: { type: 'string', description: 'Post or comment ID to vote on' }, voteDirection: { type: 'number', @@ -711,11 +882,34 @@ export const RedditBlock: BlockConfig = { deleteId: { type: 'string', description: 'Post or comment ID to delete' }, subscribeSubreddit: { type: 'string', description: 'Subreddit to subscribe/unsubscribe' }, subscribeAction: { type: 'string', description: 'Subscribe action (sub or unsub)' }, + username: { type: 'string', description: 'Reddit username to look up' }, + messageTo: { type: 'string', description: 'Message recipient' }, + messageSubject: { type: 'string', description: 'Message subject' }, + messageText: { type: 'string', description: 'Message body in markdown' }, + messageFromSr: { type: 'string', description: 'Send from subreddit (mod mail)' }, + messageWhere: { type: 'string', description: 'Message folder' }, + messageLimit: { type: 'number', description: 'Maximum messages' }, + messageMark: { type: 'boolean', description: 'Mark messages as read' }, }, outputs: { subreddit: { type: 'string', description: 'Subreddit name' }, posts: { type: 'json', description: 'Posts data' }, post: { type: 'json', description: 'Single post data' }, comments: { type: 'json', description: 'Comments data' }, + success: { type: 'boolean', description: 'Operation success status' }, + message: { type: 'string', description: 'Result message' }, + data: { type: 'json', description: 'Response data' }, + after: { type: 'string', description: 'Pagination cursor (next page)' }, + before: { type: 'string', description: 'Pagination cursor (previous page)' }, + id: { type: 'string', description: 'Entity ID' }, + name: { type: 'string', description: 'Entity fullname' }, + messages: { type: 'json', description: 'Messages data' }, + display_name: { type: 'string', description: 'Subreddit display name' }, + subscribers: { type: 'number', description: 'Subscriber count' }, + description: { type: 'string', description: 'Description text' }, + link_karma: { type: 'number', description: 'Link karma' }, + comment_karma: { type: 'number', description: 'Comment karma' }, + total_karma: { type: 'number', description: 'Total karma' }, + icon_img: { type: 'string', description: 'Icon image URL' }, }, } diff --git a/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts b/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts index 952e9ee3e60..47b0f26084f 100644 --- a/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts +++ b/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts @@ -37,13 +37,13 @@ describe('migrateSubblockIds', () => { const { blocks, migrated } = migrateSubblockIds(input) expect(migrated).toBe(true) - expect(blocks['b1'].subBlocks['knowledgeBaseSelector']).toEqual({ + expect(blocks.b1.subBlocks.knowledgeBaseSelector).toEqual({ id: 'knowledgeBaseSelector', type: 'knowledge-base-selector', value: 'kb-uuid-123', }) - expect(blocks['b1'].subBlocks['knowledgeBaseId']).toBeUndefined() - expect(blocks['b1'].subBlocks['operation'].value).toBe('search') + expect(blocks.b1.subBlocks.knowledgeBaseId).toBeUndefined() + expect(blocks.b1.subBlocks.operation.value).toBe('search') }) it('should prefer new key when both old and new exist', () => { @@ -68,8 +68,8 @@ describe('migrateSubblockIds', () => { const { blocks, migrated } = migrateSubblockIds(input) expect(migrated).toBe(true) - expect(blocks['b1'].subBlocks['knowledgeBaseSelector'].value).toBe('fresh-kb') - expect(blocks['b1'].subBlocks['knowledgeBaseId']).toBeUndefined() + expect(blocks.b1.subBlocks.knowledgeBaseSelector.value).toBe('fresh-kb') + expect(blocks.b1.subBlocks.knowledgeBaseId).toBeUndefined() }) it('should not touch blocks that already use the new key', () => { @@ -89,7 +89,7 @@ describe('migrateSubblockIds', () => { const { blocks, migrated } = migrateSubblockIds(input) expect(migrated).toBe(false) - expect(blocks['b1'].subBlocks['knowledgeBaseSelector'].value).toBe('kb-uuid') + expect(blocks.b1.subBlocks.knowledgeBaseSelector.value).toBe('kb-uuid') }) }) @@ -109,8 +109,8 @@ describe('migrateSubblockIds', () => { const { blocks } = migrateSubblockIds(input) - expect(input['b1'].subBlocks['knowledgeBaseId']).toBeDefined() - expect(blocks['b1'].subBlocks['knowledgeBaseSelector']).toBeDefined() + expect(input.b1.subBlocks.knowledgeBaseId).toBeDefined() + expect(blocks.b1.subBlocks.knowledgeBaseSelector).toBeDefined() expect(blocks).not.toBe(input) }) @@ -127,7 +127,7 @@ describe('migrateSubblockIds', () => { const { blocks, migrated } = migrateSubblockIds(input) expect(migrated).toBe(false) - expect(blocks['b1'].subBlocks['code'].value).toBe('console.log("hi")') + expect(blocks.b1.subBlocks.code.value).toBe('console.log("hi")') }) it('should migrate multiple blocks in one pass', () => { @@ -166,9 +166,9 @@ describe('migrateSubblockIds', () => { const { blocks, migrated } = migrateSubblockIds(input) expect(migrated).toBe(true) - expect(blocks['b1'].subBlocks['knowledgeBaseSelector'].value).toBe('kb-1') - expect(blocks['b2'].subBlocks['knowledgeBaseSelector'].value).toBe('kb-2') - expect(blocks['b3'].subBlocks['code']).toBeDefined() + expect(blocks.b1.subBlocks.knowledgeBaseSelector.value).toBe('kb-1') + expect(blocks.b2.subBlocks.knowledgeBaseSelector.value).toBe('kb-2') + expect(blocks.b3.subBlocks.code).toBeDefined() }) it('should handle blocks with empty subBlocks', () => { diff --git a/apps/sim/tools/reddit/delete.ts b/apps/sim/tools/reddit/delete.ts index 2f296517d93..749a5e2a5fb 100644 --- a/apps/sim/tools/reddit/delete.ts +++ b/apps/sim/tools/reddit/delete.ts @@ -46,9 +46,7 @@ export const deleteTool: ToolConfig = { id: params.id, }) - return { - body: formData.toString(), - } + return formData.toString() as unknown as Record }, }, diff --git a/apps/sim/tools/reddit/get_comments.ts b/apps/sim/tools/reddit/get_comments.ts index 83bbfe563bf..c879ea2c503 100644 --- a/apps/sim/tools/reddit/get_comments.ts +++ b/apps/sim/tools/reddit/get_comments.ts @@ -69,12 +69,6 @@ export const getCommentsTool: ToolConfig { const subreddit = normalizeSubreddit(params.subreddit) - const limit = Math.min(Math.max(1, params.limit || 10), 100) + const limit = Math.min(Math.max(1, params.limit ?? 10), 100) // Build URL with appropriate parameters using OAuth endpoint const urlParams = new URLSearchParams({ @@ -122,18 +122,19 @@ export const getControversialTool: ToolConfig { const post = child.data || {} return { - id: post.id || '', - title: post.title || '', + id: post.id ?? '', + name: post.name ?? '', + title: post.title ?? '', author: post.author || '[deleted]', - url: post.url || '', + url: post.url ?? '', permalink: post.permalink ? `https://www.reddit.com${post.permalink}` : '', - created_utc: post.created_utc || 0, - score: post.score || 0, - num_comments: post.num_comments || 0, + created_utc: post.created_utc ?? 0, + score: post.score ?? 0, + num_comments: post.num_comments ?? 0, is_self: !!post.is_self, - selftext: post.selftext || '', - thumbnail: post.thumbnail || '', - subreddit: post.subreddit || subredditName, + selftext: post.selftext ?? '', + thumbnail: post.thumbnail ?? '', + subreddit: post.subreddit ?? subredditName, } }) || [] @@ -142,6 +143,8 @@ export const getControversialTool: ToolConfig = { + id: 'reddit_get_me', + name: 'Get Reddit User Identity', + description: 'Get information about the authenticated Reddit user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + }, + + request: { + url: () => 'https://oauth.reddit.com/api/v1/me?raw_json=1', + method: 'GET', + headers: (params: RedditGetMeParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + Accept: 'application/json', + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id ?? '', + name: data.name ?? '', + created_utc: data.created_utc ?? 0, + link_karma: data.link_karma ?? 0, + comment_karma: data.comment_karma ?? 0, + total_karma: data.total_karma ?? 0, + is_gold: data.is_gold ?? false, + is_mod: data.is_mod ?? false, + has_verified_email: data.has_verified_email ?? false, + icon_img: data.icon_img ?? '', + }, + } + }, + + outputs: { + id: { type: 'string', description: 'User ID' }, + name: { type: 'string', description: 'Username' }, + created_utc: { type: 'number', description: 'Account creation time in UTC epoch seconds' }, + link_karma: { type: 'number', description: 'Total link karma' }, + comment_karma: { type: 'number', description: 'Total comment karma' }, + total_karma: { type: 'number', description: 'Combined total karma' }, + is_gold: { type: 'boolean', description: 'Whether user has Reddit Premium' }, + is_mod: { type: 'boolean', description: 'Whether user is a moderator' }, + has_verified_email: { type: 'boolean', description: 'Whether email is verified' }, + icon_img: { type: 'string', description: 'User avatar/icon URL' }, + }, +} diff --git a/apps/sim/tools/reddit/get_messages.ts b/apps/sim/tools/reddit/get_messages.ts new file mode 100644 index 00000000000..da7c4e2ff4f --- /dev/null +++ b/apps/sim/tools/reddit/get_messages.ts @@ -0,0 +1,166 @@ +import type { RedditGetMessagesParams, RedditMessagesResponse } from '@/tools/reddit/types' +import type { ToolConfig } from '@/tools/types' + +export const getMessagesTool: ToolConfig = { + id: 'reddit_get_messages', + name: 'Get Reddit Messages', + description: 'Retrieve private messages from your Reddit inbox', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + where: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Message folder to retrieve: "inbox" (all), "unread", "sent", "messages" (direct messages only), "comments" (comment replies), "selfreply" (self-post replies), or "mentions" (username mentions). Default: "inbox"', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of messages to return (e.g., 25). Default: 25, max: 100', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Fullname of a thing to fetch items after (for pagination)', + }, + before: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Fullname of a thing to fetch items before (for pagination)', + }, + mark: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to mark fetched messages as read', + }, + count: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'A count of items already seen in the listing (used for numbering)', + }, + show: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Show items that would normally be filtered (e.g., "all")', + }, + }, + + request: { + url: (params: RedditGetMessagesParams) => { + const where = params.where || 'inbox' + const limit = Math.min(Math.max(1, params.limit ?? 25), 100) + + const urlParams = new URLSearchParams({ + limit: limit.toString(), + raw_json: '1', + }) + + if (params.after) urlParams.append('after', params.after) + if (params.before) urlParams.append('before', params.before) + if (params.mark !== undefined) urlParams.append('mark', params.mark.toString()) + if (params.count !== undefined) urlParams.append('count', Number(params.count).toString()) + if (params.show) urlParams.append('show', params.show) + + return `https://oauth.reddit.com/message/${where}?${urlParams.toString()}` + }, + method: 'GET', + headers: (params: RedditGetMessagesParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + Accept: 'application/json', + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + const messages = + data.data?.children?.map((child: any) => { + const msg = child.data || {} + return { + id: msg.id ?? '', + name: msg.name ?? '', + author: msg.author ?? '', + dest: msg.dest ?? '', + subject: msg.subject ?? '', + body: msg.body ?? '', + created_utc: msg.created_utc ?? 0, + new: msg.new ?? false, + was_comment: msg.was_comment ?? false, + context: msg.context ?? '', + distinguished: msg.distinguished ?? null, + } + }) || [] + + return { + success: true, + output: { + messages, + after: data.data?.after ?? null, + before: data.data?.before ?? null, + }, + } + }, + + outputs: { + messages: { + type: 'array', + description: 'Array of messages with sender, recipient, subject, body, and metadata', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Message ID' }, + name: { type: 'string', description: 'Thing fullname (t4_xxxxx)' }, + author: { type: 'string', description: 'Sender username' }, + dest: { type: 'string', description: 'Recipient username' }, + subject: { type: 'string', description: 'Message subject' }, + body: { type: 'string', description: 'Message body text' }, + created_utc: { type: 'number', description: 'Creation time in UTC epoch seconds' }, + new: { type: 'boolean', description: 'Whether the message is unread' }, + was_comment: { type: 'boolean', description: 'Whether the message is a comment reply' }, + context: { type: 'string', description: 'Context URL for comment replies' }, + distinguished: { + type: 'string', + description: 'Distinction: null/"moderator"/"admin"', + optional: true, + }, + }, + }, + }, + after: { + type: 'string', + description: 'Fullname of the last item for forward pagination', + optional: true, + }, + before: { + type: 'string', + description: 'Fullname of the first item for backward pagination', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/reddit/get_posts.ts b/apps/sim/tools/reddit/get_posts.ts index 9afb170a200..9c38f3eb287 100644 --- a/apps/sim/tools/reddit/get_posts.ts +++ b/apps/sim/tools/reddit/get_posts.ts @@ -30,7 +30,8 @@ export const getPostsTool: ToolConfig = type: 'string', required: false, visibility: 'user-or-llm', - description: 'Sort method for posts (e.g., "hot", "new", "top", "rising"). Default: "hot"', + description: + 'Sort method for posts (e.g., "hot", "new", "top", "rising", "controversial"). Default: "hot"', }, limit: { type: 'number', @@ -43,7 +44,7 @@ export const getPostsTool: ToolConfig = required: false, visibility: 'user-or-llm', description: - 'Time filter for "top" sorted posts: "day", "week", "month", "year", or "all" (default: "day")', + 'Time filter for "top" sorted posts: "day", "week", "month", "year", or "all" (default: "all")', }, after: { type: 'string', @@ -75,13 +76,19 @@ export const getPostsTool: ToolConfig = visibility: 'user-or-llm', description: 'Expand subreddit details in the response', }, + g: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Geo filter for posts (e.g., "GLOBAL", "US", "AR", etc.)', + }, }, request: { url: (params: RedditPostsParams) => { const subreddit = normalizeSubreddit(params.subreddit) const sort = params.sort || 'hot' - const limit = Math.min(Math.max(1, params.limit || 10), 100) + const limit = Math.min(Math.max(1, params.limit ?? 10), 100) // Build URL with appropriate parameters using OAuth endpoint const urlParams = new URLSearchParams({ @@ -89,8 +96,12 @@ export const getPostsTool: ToolConfig = raw_json: '1', }) - // Add time parameter only for 'top' sorting - if (sort === 'top' && params.time !== undefined && params.time !== null) { + // Add time parameter for 'top' and 'controversial' sorting + if ( + (sort === 'top' || sort === 'controversial') && + params.time !== undefined && + params.time !== null + ) { urlParams.append('t', params.time) } @@ -105,6 +116,7 @@ export const getPostsTool: ToolConfig = urlParams.append('show', params.show) if (params.sr_detail !== undefined && params.sr_detail !== null) urlParams.append('sr_detail', params.sr_detail.toString()) + if (params.g) urlParams.append('g', params.g) return `https://oauth.reddit.com/r/${subreddit}/${sort}?${urlParams.toString()}` }, @@ -134,18 +146,19 @@ export const getPostsTool: ToolConfig = data.data?.children?.map((child: any) => { const post = child.data || {} return { - id: post.id || '', - title: post.title || '', + id: post.id ?? '', + name: post.name ?? '', + title: post.title ?? '', author: post.author || '[deleted]', - url: post.url || '', + url: post.url ?? '', permalink: post.permalink ? `https://www.reddit.com${post.permalink}` : '', - created_utc: post.created_utc || 0, - score: post.score || 0, - num_comments: post.num_comments || 0, + created_utc: post.created_utc ?? 0, + score: post.score ?? 0, + num_comments: post.num_comments ?? 0, is_self: !!post.is_self, - selftext: post.selftext || '', - thumbnail: post.thumbnail || '', - subreddit: post.subreddit || subredditName, + selftext: post.selftext ?? '', + thumbnail: post.thumbnail ?? '', + subreddit: post.subreddit ?? subredditName, } }) || [] @@ -154,6 +167,8 @@ export const getPostsTool: ToolConfig = output: { subreddit: subredditName, posts, + after: data.data?.after ?? null, + before: data.data?.before ?? null, }, } }, @@ -170,6 +185,7 @@ export const getPostsTool: ToolConfig = type: 'object', properties: { id: { type: 'string', description: 'Post ID' }, + name: { type: 'string', description: 'Thing fullname (t3_xxxxx)' }, title: { type: 'string', description: 'Post title' }, author: { type: 'string', description: 'Author username' }, url: { type: 'string', description: 'Post URL' }, @@ -184,5 +200,15 @@ export const getPostsTool: ToolConfig = }, }, }, + after: { + type: 'string', + description: 'Fullname of the last item for forward pagination', + optional: true, + }, + before: { + type: 'string', + description: 'Fullname of the first item for backward pagination', + optional: true, + }, }, } diff --git a/apps/sim/tools/reddit/get_subreddit_info.ts b/apps/sim/tools/reddit/get_subreddit_info.ts new file mode 100644 index 00000000000..ae81f9c57d5 --- /dev/null +++ b/apps/sim/tools/reddit/get_subreddit_info.ts @@ -0,0 +1,102 @@ +import type { + RedditGetSubredditInfoParams, + RedditSubredditInfoResponse, +} from '@/tools/reddit/types' +import { normalizeSubreddit } from '@/tools/reddit/utils' +import type { ToolConfig } from '@/tools/types' + +export const getSubredditInfoTool: ToolConfig< + RedditGetSubredditInfoParams, + RedditSubredditInfoResponse +> = { + id: 'reddit_get_subreddit_info', + name: 'Get Subreddit Info', + description: 'Get metadata and information about a subreddit', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + subreddit: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The subreddit to get info about (e.g., "technology", "programming", "news")', + }, + }, + + request: { + url: (params: RedditGetSubredditInfoParams) => { + const subreddit = normalizeSubreddit(params.subreddit) + return `https://oauth.reddit.com/r/${subreddit}/about?raw_json=1` + }, + method: 'GET', + headers: (params: RedditGetSubredditInfoParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + Accept: 'application/json', + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const sub = data.data || data + + return { + success: true, + output: { + id: sub.id ?? '', + name: sub.name ?? '', + display_name: sub.display_name ?? '', + title: sub.title ?? '', + description: sub.description ?? '', + public_description: sub.public_description ?? '', + subscribers: sub.subscribers ?? 0, + accounts_active: sub.accounts_active ?? 0, + created_utc: sub.created_utc ?? 0, + over18: sub.over18 ?? false, + lang: sub.lang ?? '', + subreddit_type: sub.subreddit_type ?? '', + url: sub.url ?? '', + icon_img: sub.icon_img ?? null, + banner_img: sub.banner_img ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Subreddit ID' }, + name: { type: 'string', description: 'Subreddit fullname (t5_xxxxx)' }, + display_name: { type: 'string', description: 'Subreddit name without prefix' }, + title: { type: 'string', description: 'Subreddit title' }, + description: { type: 'string', description: 'Full subreddit description (markdown)' }, + public_description: { type: 'string', description: 'Short public description' }, + subscribers: { type: 'number', description: 'Number of subscribers' }, + accounts_active: { type: 'number', description: 'Number of currently active users' }, + created_utc: { type: 'number', description: 'Creation time in UTC epoch seconds' }, + over18: { type: 'boolean', description: 'Whether the subreddit is NSFW' }, + lang: { type: 'string', description: 'Primary language of the subreddit' }, + subreddit_type: { + type: 'string', + description: 'Subreddit type: public, private, restricted, etc.', + }, + url: { type: 'string', description: 'Subreddit URL path (e.g., /r/technology/)' }, + icon_img: { type: 'string', description: 'Subreddit icon URL', optional: true }, + banner_img: { type: 'string', description: 'Subreddit banner URL', optional: true }, + }, +} diff --git a/apps/sim/tools/reddit/get_user.ts b/apps/sim/tools/reddit/get_user.ts new file mode 100644 index 00000000000..91a68592fc6 --- /dev/null +++ b/apps/sim/tools/reddit/get_user.ts @@ -0,0 +1,82 @@ +import type { RedditGetUserParams, RedditUserResponse } from '@/tools/reddit/types' +import type { ToolConfig } from '@/tools/types' + +export const getUserTool: ToolConfig = { + id: 'reddit_get_user', + name: 'Get Reddit User Profile', + description: 'Get public profile information about any Reddit user by username', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + username: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Reddit username to look up (e.g., "spez", "example_user")', + }, + }, + + request: { + url: (params: RedditGetUserParams) => { + const username = params.username.trim().replace(/^u\//, '') + return `https://oauth.reddit.com/user/${username}/about?raw_json=1` + }, + method: 'GET', + headers: (params: RedditGetUserParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + Accept: 'application/json', + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const user = data.data || data + + return { + success: true, + output: { + id: user.id ?? '', + name: user.name ?? '', + created_utc: user.created_utc ?? 0, + link_karma: user.link_karma ?? 0, + comment_karma: user.comment_karma ?? 0, + total_karma: user.total_karma ?? 0, + is_gold: user.is_gold ?? false, + is_mod: user.is_mod ?? false, + has_verified_email: user.has_verified_email ?? false, + icon_img: user.icon_img ?? '', + }, + } + }, + + outputs: { + id: { type: 'string', description: 'User ID' }, + name: { type: 'string', description: 'Username' }, + created_utc: { type: 'number', description: 'Account creation time in UTC epoch seconds' }, + link_karma: { type: 'number', description: 'Total link karma' }, + comment_karma: { type: 'number', description: 'Total comment karma' }, + total_karma: { type: 'number', description: 'Combined total karma' }, + is_gold: { type: 'boolean', description: 'Whether user has Reddit Premium' }, + is_mod: { type: 'boolean', description: 'Whether user is a moderator' }, + has_verified_email: { type: 'boolean', description: 'Whether email is verified' }, + icon_img: { type: 'string', description: 'User avatar/icon URL' }, + }, +} diff --git a/apps/sim/tools/reddit/hot_posts.ts b/apps/sim/tools/reddit/hot_posts.ts index 0dfc486ecd6..30baa551447 100644 --- a/apps/sim/tools/reddit/hot_posts.ts +++ b/apps/sim/tools/reddit/hot_posts.ts @@ -43,7 +43,7 @@ export const hotPostsTool: ToolConfig = request: { url: (params) => { const subreddit = normalizeSubreddit(params.subreddit) - const limit = Math.min(Math.max(1, params.limit || 10), 100) + const limit = Math.min(Math.max(1, params.limit ?? 10), 100) return `https://oauth.reddit.com/r/${subreddit}/hot?limit=${limit}&raw_json=1` }, @@ -65,25 +65,26 @@ export const hotPostsTool: ToolConfig = const data = await response.json() // Process the posts data with proper error handling - const posts: RedditPost[] = data.data.children.map((child: any) => { - const post = child.data || {} - return { - id: post.id || '', - title: post.title || '', - author: post.author || '[deleted]', - url: post.url || '', - permalink: post.permalink ? `https://www.reddit.com${post.permalink}` : '', - created_utc: post.created_utc || 0, - score: post.score || 0, - num_comments: post.num_comments || 0, - selftext: post.selftext || '', - thumbnail: - post.thumbnail !== 'self' && post.thumbnail !== 'default' ? post.thumbnail : undefined, - is_self: !!post.is_self, - subreddit: post.subreddit || requestParams?.subreddit || '', - subreddit_name_prefixed: post.subreddit_name_prefixed || '', - } - }) + const posts: RedditPost[] = + data.data?.children?.map((child: any) => { + const post = child.data || {} + return { + id: post.id ?? '', + name: post.name ?? '', + title: post.title ?? '', + author: post.author || '[deleted]', + url: post.url ?? '', + permalink: post.permalink ? `https://www.reddit.com${post.permalink}` : '', + created_utc: post.created_utc ?? 0, + score: post.score ?? 0, + num_comments: post.num_comments ?? 0, + selftext: post.selftext ?? '', + thumbnail: + post.thumbnail !== 'self' && post.thumbnail !== 'default' ? post.thumbnail : undefined, + is_self: !!post.is_self, + subreddit: post.subreddit ?? requestParams?.subreddit ?? '', + } + }) || [] // Extract the subreddit name from the response data with fallback const subreddit = @@ -95,6 +96,8 @@ export const hotPostsTool: ToolConfig = output: { subreddit, posts, + after: data.data?.after ?? null, + before: data.data?.before ?? null, }, } }, @@ -112,6 +115,7 @@ export const hotPostsTool: ToolConfig = type: 'object', properties: { id: { type: 'string', description: 'Post ID' }, + name: { type: 'string', description: 'Thing fullname (t3_xxxxx)' }, title: { type: 'string', description: 'Post title' }, author: { type: 'string', description: 'Author username' }, url: { type: 'string', description: 'Post URL' }, @@ -123,9 +127,18 @@ export const hotPostsTool: ToolConfig = selftext: { type: 'string', description: 'Text content for self posts' }, thumbnail: { type: 'string', description: 'Thumbnail URL' }, subreddit: { type: 'string', description: 'Subreddit name' }, - subreddit_name_prefixed: { type: 'string', description: 'Subreddit name with r/ prefix' }, }, }, }, + after: { + type: 'string', + description: 'Fullname of the last item for forward pagination', + optional: true, + }, + before: { + type: 'string', + description: 'Fullname of the first item for backward pagination', + optional: true, + }, }, } diff --git a/apps/sim/tools/reddit/index.ts b/apps/sim/tools/reddit/index.ts index ca1a816703c..23022b19b5f 100644 --- a/apps/sim/tools/reddit/index.ts +++ b/apps/sim/tools/reddit/index.ts @@ -2,11 +2,16 @@ import { deleteTool } from '@/tools/reddit/delete' import { editTool } from '@/tools/reddit/edit' import { getCommentsTool } from '@/tools/reddit/get_comments' import { getControversialTool } from '@/tools/reddit/get_controversial' +import { getMeTool } from '@/tools/reddit/get_me' +import { getMessagesTool } from '@/tools/reddit/get_messages' import { getPostsTool } from '@/tools/reddit/get_posts' +import { getSubredditInfoTool } from '@/tools/reddit/get_subreddit_info' +import { getUserTool } from '@/tools/reddit/get_user' import { hotPostsTool } from '@/tools/reddit/hot_posts' import { replyTool } from '@/tools/reddit/reply' import { saveTool, unsaveTool } from '@/tools/reddit/save' import { searchTool } from '@/tools/reddit/search' +import { sendMessageTool } from '@/tools/reddit/send_message' import { submitPostTool } from '@/tools/reddit/submit_post' import { subscribeTool } from '@/tools/reddit/subscribe' import { voteTool } from '@/tools/reddit/vote' @@ -24,3 +29,8 @@ export const redditReplyTool = replyTool export const redditEditTool = editTool export const redditDeleteTool = deleteTool export const redditSubscribeTool = subscribeTool +export const redditGetMeTool = getMeTool +export const redditGetUserTool = getUserTool +export const redditSendMessageTool = sendMessageTool +export const redditGetMessagesTool = getMessagesTool +export const redditGetSubredditInfoTool = getSubredditInfoTool diff --git a/apps/sim/tools/reddit/reply.ts b/apps/sim/tools/reddit/reply.ts index d22fceb07e5..241a08da174 100644 --- a/apps/sim/tools/reddit/reply.ts +++ b/apps/sim/tools/reddit/reply.ts @@ -32,6 +32,12 @@ export const replyTool: ToolConfig = { visibility: 'user-or-llm', description: 'Comment text in markdown format (e.g., "Great post! Here is my **reply**")', }, + return_rtjson: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Return response in Rich Text JSON format', + }, }, request: { @@ -55,6 +61,9 @@ export const replyTool: ToolConfig = { api_type: 'json', }) + if (params.return_rtjson !== undefined) + formData.append('return_rtjson', params.return_rtjson.toString()) + return formData.toString() as unknown as Record }, }, diff --git a/apps/sim/tools/reddit/search.ts b/apps/sim/tools/reddit/search.ts index c2bcafebef1..bbac78b0ea1 100644 --- a/apps/sim/tools/reddit/search.ts +++ b/apps/sim/tools/reddit/search.ts @@ -83,13 +83,26 @@ export const searchTool: ToolConfig = { visibility: 'user-or-llm', description: 'Show items that would normally be filtered (e.g., "all")', }, + type: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Type of search results: "link" (posts), "sr" (subreddits), or "user" (users). Default: "link"', + }, + sr_detail: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Expand subreddit details in the response', + }, }, request: { url: (params: RedditSearchParams) => { const subreddit = normalizeSubreddit(params.subreddit) const sort = params.sort || 'relevance' - const limit = Math.min(Math.max(1, params.limit || 10), 100) + const limit = Math.min(Math.max(1, params.limit ?? 10), 100) const restrict_sr = params.restrict_sr !== false // Default to true // Build URL with appropriate parameters using OAuth endpoint @@ -111,6 +124,8 @@ export const searchTool: ToolConfig = { if (params.before) urlParams.append('before', params.before) if (params.count !== undefined) urlParams.append('count', Number(params.count).toString()) if (params.show) urlParams.append('show', params.show) + if (params.type) urlParams.append('type', params.type) + if (params.sr_detail !== undefined) urlParams.append('sr_detail', params.sr_detail.toString()) return `https://oauth.reddit.com/r/${subreddit}/search?${urlParams.toString()}` }, @@ -140,18 +155,19 @@ export const searchTool: ToolConfig = { data.data?.children?.map((child: any) => { const post = child.data || {} return { - id: post.id || '', - title: post.title || '', + id: post.id ?? '', + name: post.name ?? '', + title: post.title ?? '', author: post.author || '[deleted]', - url: post.url || '', + url: post.url ?? '', permalink: post.permalink ? `https://www.reddit.com${post.permalink}` : '', - created_utc: post.created_utc || 0, - score: post.score || 0, - num_comments: post.num_comments || 0, + created_utc: post.created_utc ?? 0, + score: post.score ?? 0, + num_comments: post.num_comments ?? 0, is_self: !!post.is_self, - selftext: post.selftext || '', - thumbnail: post.thumbnail || '', - subreddit: post.subreddit || subredditName, + selftext: post.selftext ?? '', + thumbnail: post.thumbnail ?? '', + subreddit: post.subreddit ?? subredditName, } }) || [] @@ -160,6 +176,8 @@ export const searchTool: ToolConfig = { output: { subreddit: subredditName, posts, + after: data.data?.after ?? null, + before: data.data?.before ?? null, }, } }, @@ -177,6 +195,7 @@ export const searchTool: ToolConfig = { type: 'object', properties: { id: { type: 'string', description: 'Post ID' }, + name: { type: 'string', description: 'Thing fullname (t3_xxxxx)' }, title: { type: 'string', description: 'Post title' }, author: { type: 'string', description: 'Author username' }, url: { type: 'string', description: 'Post URL' }, @@ -191,5 +210,15 @@ export const searchTool: ToolConfig = { }, }, }, + after: { + type: 'string', + description: 'Fullname of the last item for forward pagination', + optional: true, + }, + before: { + type: 'string', + description: 'Fullname of the first item for backward pagination', + optional: true, + }, }, } diff --git a/apps/sim/tools/reddit/send_message.ts b/apps/sim/tools/reddit/send_message.ts new file mode 100644 index 00000000000..d53c88f52c6 --- /dev/null +++ b/apps/sim/tools/reddit/send_message.ts @@ -0,0 +1,111 @@ +import type { RedditSendMessageParams, RedditWriteResponse } from '@/tools/reddit/types' +import type { ToolConfig } from '@/tools/types' + +export const sendMessageTool: ToolConfig = { + id: 'reddit_send_message', + name: 'Send Reddit Message', + description: 'Send a private message to a Reddit user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + to: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Recipient username (e.g., "example_user") or subreddit (e.g., "/r/subreddit")', + }, + subject: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Message subject (max 100 characters)', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Message body in markdown format', + }, + from_sr: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Subreddit name to send the message from (requires moderator mail permission)', + }, + }, + + request: { + url: () => 'https://oauth.reddit.com/api/compose', + method: 'POST', + headers: (params: RedditSendMessageParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + 'Content-Type': 'application/x-www-form-urlencoded', + } + }, + body: (params: RedditSendMessageParams) => { + const formData = new URLSearchParams({ + to: params.to.trim(), + subject: params.subject, + text: params.text, + api_type: 'json', + }) + + if (params.from_sr) { + formData.append('from_sr', params.from_sr.trim()) + } + + return formData.toString() as unknown as Record + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (data.json?.errors && data.json.errors.length > 0) { + const errors = data.json.errors.map((err: any) => err.join(': ')).join(', ') + return { + success: false, + output: { + success: false, + message: `Failed to send message: ${errors}`, + }, + } + } + + return { + success: true, + output: { + success: true, + message: 'Message sent successfully', + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the message was sent successfully', + }, + message: { + type: 'string', + description: 'Success or error message', + }, + }, +} diff --git a/apps/sim/tools/reddit/submit_post.ts b/apps/sim/tools/reddit/submit_post.ts index e08bae50a26..88cce3e8c37 100644 --- a/apps/sim/tools/reddit/submit_post.ts +++ b/apps/sim/tools/reddit/submit_post.ts @@ -64,6 +64,24 @@ export const submitPostTool: ToolConfig visibility: 'user-or-llm', description: 'Send reply notifications to inbox (default: true)', }, + flair_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Flair template UUID for the post (max 36 characters)', + }, + flair_text: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Flair text to display on the post (max 64 characters)', + }, + collection_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Collection UUID to add the post to', + }, }, request: { @@ -105,6 +123,9 @@ export const submitPostTool: ToolConfig // Add optional parameters if (params.nsfw !== undefined) formData.append('nsfw', params.nsfw.toString()) if (params.spoiler !== undefined) formData.append('spoiler', params.spoiler.toString()) + if (params.flair_id) formData.append('flair_id', params.flair_id) + if (params.flair_text) formData.append('flair_text', params.flair_text) + if (params.collection_id) formData.append('collection_id', params.collection_id) if (params.send_replies !== undefined) formData.append('sendreplies', params.send_replies.toString()) @@ -138,7 +159,9 @@ export const submitPostTool: ToolConfig id: postData?.id, name: postData?.name, url: postData?.url, - permalink: `https://www.reddit.com${postData?.url}`, + permalink: postData?.permalink + ? `https://www.reddit.com${postData.permalink}` + : (postData?.url ?? ''), }, }, } diff --git a/apps/sim/tools/reddit/types.ts b/apps/sim/tools/reddit/types.ts index 40a7b985873..28395fb7031 100644 --- a/apps/sim/tools/reddit/types.ts +++ b/apps/sim/tools/reddit/types.ts @@ -11,12 +11,14 @@ import type { OutputProperty, ToolResponse } from '@/tools/types' */ export const POST_OUTPUT_PROPERTIES = { id: { type: 'string', description: 'Post ID' }, + name: { type: 'string', description: 'Thing fullname (t3_xxxxx)' }, title: { type: 'string', description: 'Post title (may contain newlines)' }, author: { type: 'string', description: 'Poster account name (null for promotional links)' }, url: { type: 'string', description: 'External link URL or self-post permalink' }, permalink: { type: 'string', description: 'Relative permanent link URL' }, created_utc: { type: 'number', description: 'Creation time in UTC epoch seconds' }, score: { type: 'number', description: 'Net upvotes minus downvotes' }, + upvote_ratio: { type: 'number', description: 'Ratio of upvotes to total votes' }, num_comments: { type: 'number', description: 'Total comments including removed ones' }, is_self: { type: 'boolean', description: 'Indicates self-post vs external link' }, selftext: { @@ -55,6 +57,7 @@ export const POST_OUTPUT_PROPERTIES = { */ export const COMMENT_OUTPUT_PROPERTIES = { id: { type: 'string', description: 'Comment ID' }, + name: { type: 'string', description: 'Thing fullname (t1_xxxxx)' }, author: { type: 'string', description: 'Commenter account name' }, body: { type: 'string', description: 'Raw unformatted comment text with markup characters' }, body_html: { type: 'string', description: 'Formatted HTML version of comment' }, @@ -70,6 +73,7 @@ export const COMMENT_OUTPUT_PROPERTIES = { type: 'string', description: 'Distinction: null/"moderator"/"admin"/"special"', }, + is_submitter: { type: 'boolean', description: 'Whether commenter is the post author' }, ups: { type: 'number', description: 'Upvote count' }, downs: { type: 'number', description: 'Downvote count' }, likes: { type: 'boolean', description: 'User vote: true (up), false (down), null (none)' }, @@ -88,6 +92,7 @@ export const COMMENT_OUTPUT_PROPERTIES = { */ export const POST_LISTING_OUTPUT_PROPERTIES = { id: { type: 'string', description: 'Post ID' }, + name: { type: 'string', description: 'Thing fullname (t3_xxxxx)' }, title: { type: 'string', description: 'Post title' }, author: { type: 'string', description: 'Author username' }, url: { type: 'string', description: 'Post URL' }, @@ -106,6 +111,7 @@ export const POST_LISTING_OUTPUT_PROPERTIES = { */ export const COMMENT_LISTING_OUTPUT_PROPERTIES = { id: { type: 'string', description: 'Comment ID' }, + name: { type: 'string', description: 'Thing fullname (t1_xxxxx)' }, author: { type: 'string', description: 'Comment author' }, body: { type: 'string', description: 'Comment text' }, score: { type: 'number', description: 'Comment score' }, @@ -130,6 +136,7 @@ export const COMMENT_WITH_REPLIES_OUTPUT_PROPERTIES = { */ export const POST_METADATA_OUTPUT_PROPERTIES = { id: { type: 'string', description: 'Post ID' }, + name: { type: 'string', description: 'Thing fullname (t3_xxxxx)' }, title: { type: 'string', description: 'Post title' }, author: { type: 'string', description: 'Post author' }, selftext: { type: 'string', description: 'Post text content' }, @@ -235,8 +242,86 @@ export const EDIT_DATA_OUTPUT: OutputProperty = { properties: EDIT_DATA_OUTPUT_PROPERTIES, } +/** + * Pagination cursor output properties for listing responses + */ +export const PAGINATION_OUTPUT_PROPERTIES = { + after: { + type: 'string', + description: 'Fullname of the last item for forward pagination', + optional: true, + }, + before: { + type: 'string', + description: 'Fullname of the first item for backward pagination', + optional: true, + }, +} as const satisfies Record + +/** + * User profile output properties + */ +export const USER_PROFILE_OUTPUT_PROPERTIES = { + id: { type: 'string', description: 'User ID' }, + name: { type: 'string', description: 'Username' }, + created_utc: { type: 'number', description: 'Account creation time in UTC epoch seconds' }, + link_karma: { type: 'number', description: 'Total link karma' }, + comment_karma: { type: 'number', description: 'Total comment karma' }, + total_karma: { type: 'number', description: 'Combined total karma' }, + is_gold: { type: 'boolean', description: 'Whether user has Reddit Premium' }, + is_mod: { type: 'boolean', description: 'Whether user is a moderator' }, + has_verified_email: { type: 'boolean', description: 'Whether email is verified' }, + icon_img: { type: 'string', description: 'User avatar/icon URL' }, +} as const satisfies Record + +/** + * Message output properties + */ +export const MESSAGE_OUTPUT_PROPERTIES = { + id: { type: 'string', description: 'Message ID' }, + name: { type: 'string', description: 'Thing fullname (t4_xxxxx)' }, + author: { type: 'string', description: 'Sender username' }, + dest: { type: 'string', description: 'Recipient username' }, + subject: { type: 'string', description: 'Message subject' }, + body: { type: 'string', description: 'Message body text' }, + created_utc: { type: 'number', description: 'Creation time in UTC epoch seconds' }, + new: { type: 'boolean', description: 'Whether the message is unread' }, + was_comment: { type: 'boolean', description: 'Whether the message is a comment reply' }, + context: { type: 'string', description: 'Context URL for comment replies' }, + distinguished: { + type: 'string', + description: 'Distinction: null/"moderator"/"admin"', + optional: true, + }, +} as const satisfies Record + +/** + * Subreddit info output properties + */ +export const SUBREDDIT_INFO_OUTPUT_PROPERTIES = { + id: { type: 'string', description: 'Subreddit ID' }, + name: { type: 'string', description: 'Subreddit fullname (t5_xxxxx)' }, + display_name: { type: 'string', description: 'Subreddit name without prefix' }, + title: { type: 'string', description: 'Subreddit title' }, + description: { type: 'string', description: 'Full subreddit description (markdown)' }, + public_description: { type: 'string', description: 'Short public description' }, + subscribers: { type: 'number', description: 'Number of subscribers' }, + accounts_active: { type: 'number', description: 'Number of currently active users' }, + created_utc: { type: 'number', description: 'Creation time in UTC epoch seconds' }, + over18: { type: 'boolean', description: 'Whether the subreddit is NSFW' }, + lang: { type: 'string', description: 'Primary language of the subreddit' }, + subreddit_type: { + type: 'string', + description: 'Subreddit type: public, private, restricted, etc.', + }, + url: { type: 'string', description: 'Subreddit URL path (e.g., /r/technology/)' }, + icon_img: { type: 'string', description: 'Subreddit icon URL', optional: true }, + banner_img: { type: 'string', description: 'Subreddit banner URL', optional: true }, +} as const satisfies Record + export interface RedditPost { id: string + name: string title: string author: string url: string @@ -248,11 +333,11 @@ export interface RedditPost { thumbnail?: string is_self: boolean subreddit: string - subreddit_name_prefixed: string } export interface RedditComment { id: string + name: string author: string body: string created_utc: number @@ -261,62 +346,72 @@ export interface RedditComment { replies: RedditComment[] } +export interface RedditMessage { + id: string + name: string + author: string + dest: string + subject: string + body: string + created_utc: number + new: boolean + was_comment: boolean + context: string + distinguished: string | null +} + export interface RedditHotPostsResponse extends ToolResponse { output: { subreddit: string posts: RedditPost[] + after: string | null + before: string | null } } -// Parameters for the generalized get_posts tool export interface RedditPostsParams { subreddit: string - sort?: 'hot' | 'new' | 'top' | 'rising' + sort?: 'hot' | 'new' | 'top' | 'rising' | 'controversial' limit?: number - time?: 'day' | 'week' | 'month' | 'year' | 'all' - // Pagination parameters + time?: 'hour' | 'day' | 'week' | 'month' | 'year' | 'all' after?: string before?: string count?: number show?: string sr_detail?: boolean + g?: string accessToken?: string } -// Response for the generalized get_posts tool export interface RedditPostsResponse extends ToolResponse { output: { subreddit: string posts: RedditPost[] + after: string | null + before: string | null } } -// Parameters for the get_comments tool export interface RedditCommentsParams { postId: string subreddit: string sort?: 'confidence' | 'top' | 'new' | 'controversial' | 'old' | 'random' | 'qa' limit?: number - // Comment-specific parameters depth?: number context?: number showedits?: boolean showmore?: boolean - showtitle?: boolean threaded?: boolean truncate?: number - // Pagination parameters - after?: string - before?: string - count?: number + comment?: string accessToken?: string } -// Response for the get_comments tool export interface RedditCommentsResponse extends ToolResponse { output: { post: { id: string + name: string title: string author: string selftext?: string @@ -328,7 +423,6 @@ export interface RedditCommentsResponse extends ToolResponse { } } -// Parameters for controversial posts export interface RedditControversialParams { subreddit: string time?: 'hour' | 'day' | 'week' | 'month' | 'year' | 'all' @@ -341,7 +435,6 @@ export interface RedditControversialParams { accessToken?: string } -// Parameters for search export interface RedditSearchParams { subreddit: string query: string @@ -353,10 +446,11 @@ export interface RedditSearchParams { count?: number show?: string restrict_sr?: boolean + type?: 'link' | 'sr' | 'user' + sr_detail?: boolean accessToken?: string } -// Parameters for submit post export interface RedditSubmitParams { subreddit: string title: string @@ -365,51 +459,81 @@ export interface RedditSubmitParams { nsfw?: boolean spoiler?: boolean send_replies?: boolean + flair_id?: string + flair_text?: string + collection_id?: string accessToken?: string } -// Parameters for vote export interface RedditVoteParams { - id: string // Thing fullname (e.g., t3_xxxxx for post, t1_xxxxx for comment) - dir: 1 | 0 | -1 // 1 = upvote, 0 = unvote, -1 = downvote + id: string + dir: 1 | 0 | -1 accessToken?: string } -// Parameters for save/unsave export interface RedditSaveParams { - id: string // Thing fullname - category?: string // Save category + id: string + category?: string accessToken?: string } -// Parameters for reply export interface RedditReplyParams { - parent_id: string // Thing fullname to reply to - text: string // Comment text in markdown + parent_id: string + text: string + return_rtjson?: boolean accessToken?: string } -// Parameters for edit export interface RedditEditParams { - thing_id: string // Thing fullname to edit - text: string // New text in markdown + thing_id: string + text: string accessToken?: string } -// Parameters for delete export interface RedditDeleteParams { - id: string // Thing fullname to delete + id: string accessToken?: string } -// Parameters for subscribe/unsubscribe export interface RedditSubscribeParams { subreddit: string action: 'sub' | 'unsub' accessToken?: string } -// Generic success response for write operations +export interface RedditGetMeParams { + accessToken?: string +} + +export interface RedditGetUserParams { + username: string + accessToken?: string +} + +export interface RedditSendMessageParams { + to: string + subject: string + text: string + from_sr?: string + accessToken?: string +} + +export interface RedditGetMessagesParams { + where?: 'inbox' | 'unread' | 'sent' | 'messages' | 'comments' | 'selfreply' | 'mentions' + limit?: number + after?: string + before?: string + mark?: boolean + count?: number + show?: string + accessToken?: string +} + +export interface RedditGetSubredditInfoParams { + subreddit: string + accessToken?: string +} + export interface RedditWriteResponse extends ToolResponse { output: { success: boolean @@ -418,8 +542,54 @@ export interface RedditWriteResponse extends ToolResponse { } } +export interface RedditUserResponse extends ToolResponse { + output: { + id: string + name: string + created_utc: number + link_karma: number + comment_karma: number + total_karma: number + is_gold: boolean + is_mod: boolean + has_verified_email: boolean + icon_img: string + } +} + +export interface RedditMessagesResponse extends ToolResponse { + output: { + messages: RedditMessage[] + after: string | null + before: string | null + } +} + +export interface RedditSubredditInfoResponse extends ToolResponse { + output: { + id: string + name: string + display_name: string + title: string + description: string + public_description: string + subscribers: number + accounts_active: number + created_utc: number + over18: boolean + lang: string + subreddit_type: string + url: string + icon_img: string | null + banner_img: string | null + } +} + export type RedditResponse = | RedditHotPostsResponse | RedditPostsResponse | RedditCommentsResponse | RedditWriteResponse + | RedditUserResponse + | RedditMessagesResponse + | RedditSubredditInfoResponse diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 8015599df29..6a7b32f5503 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1606,11 +1606,16 @@ import { redditEditTool, redditGetCommentsTool, redditGetControversialTool, + redditGetMessagesTool, + redditGetMeTool, redditGetPostsTool, + redditGetSubredditInfoTool, + redditGetUserTool, redditHotPostsTool, redditReplyTool, redditSaveTool, redditSearchTool, + redditSendMessageTool, redditSubmitPostTool, redditSubscribeTool, redditUnsaveTool, @@ -3169,6 +3174,11 @@ export const tools: Record = { reddit_edit: redditEditTool, reddit_delete: redditDeleteTool, reddit_subscribe: redditSubscribeTool, + reddit_get_me: redditGetMeTool, + reddit_get_user: redditGetUserTool, + reddit_send_message: redditSendMessageTool, + reddit_get_messages: redditGetMessagesTool, + reddit_get_subreddit_info: redditGetSubredditInfoTool, redis_get: redisGetTool, redis_set: redisSetTool, redis_delete: redisDeleteTool, From 84f806c194386e88e5acbb7df8042896c7381ccb Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 19:33:49 -0800 Subject: [PATCH 2/5] fix(reddit): add optional chaining, pagination wiring, and trim safety - Add optional chaining on children?.[0] in get_posts, get_controversial, search, and get_comments to prevent TypeError on unexpected API responses - Wire after/before pagination params to get_messages block operation - Use ?? instead of || for get_comments limit to handle 0 correctly - Add .trim() on postId in get_comments URL path --- apps/docs/content/docs/en/tools/reddit.mdx | 178 ++++++++++++++++++++- apps/sim/blocks/blocks/reddit.ts | 6 +- apps/sim/tools/reddit/get_comments.ts | 6 +- apps/sim/tools/reddit/get_controversial.ts | 2 +- apps/sim/tools/reddit/get_posts.ts | 2 +- apps/sim/tools/reddit/search.ts | 2 +- 6 files changed, 181 insertions(+), 15 deletions(-) diff --git a/apps/docs/content/docs/en/tools/reddit.mdx b/apps/docs/content/docs/en/tools/reddit.mdx index cf466f5089c..bce451955c0 100644 --- a/apps/docs/content/docs/en/tools/reddit.mdx +++ b/apps/docs/content/docs/en/tools/reddit.mdx @@ -24,7 +24,7 @@ These operations let your agents access and analyze Reddit content as part of yo ## Usage Instructions -Integrate Reddit into workflows. Read posts, comments, and search content. Submit posts, vote, reply, edit, and manage your Reddit account. +Integrate Reddit into workflows. Read posts, comments, and search content. Submit posts, vote, reply, edit, manage messages, and access user and subreddit info. @@ -39,14 +39,15 @@ Fetch posts from a subreddit with different sorting options | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `subreddit` | string | Yes | The subreddit to fetch posts from \(e.g., "technology", "news"\) | -| `sort` | string | No | Sort method for posts \(e.g., "hot", "new", "top", "rising"\). Default: "hot" | +| `sort` | string | No | Sort method for posts \(e.g., "hot", "new", "top", "rising", "controversial"\). Default: "hot" | | `limit` | number | No | Maximum number of posts to return \(e.g., 25\). Default: 10, max: 100 | -| `time` | string | No | Time filter for "top" sorted posts: "day", "week", "month", "year", or "all" \(default: "day"\) | +| `time` | string | No | Time filter for "top" sorted posts: "day", "week", "month", "year", or "all" \(default: "all"\) | | `after` | string | No | Fullname of a thing to fetch items after \(for pagination\) | | `before` | string | No | Fullname of a thing to fetch items before \(for pagination\) | | `count` | number | No | A count of items already seen in the listing \(used for numbering\) | | `show` | string | No | Show items that would normally be filtered \(e.g., "all"\) | | `sr_detail` | boolean | No | Expand subreddit details in the response | +| `g` | string | No | Geo filter for posts \(e.g., "GLOBAL", "US", "AR", etc.\) | #### Output @@ -55,6 +56,7 @@ Fetch posts from a subreddit with different sorting options | `subreddit` | string | Name of the subreddit where posts were fetched from | | `posts` | array | Array of posts with title, author, URL, score, comments count, and metadata | | ↳ `id` | string | Post ID | +| ↳ `name` | string | Thing fullname \(t3_xxxxx\) | | ↳ `title` | string | Post title | | ↳ `author` | string | Author username | | ↳ `url` | string | Post URL | @@ -66,6 +68,8 @@ Fetch posts from a subreddit with different sorting options | ↳ `selftext` | string | Text content for self posts | | ↳ `thumbnail` | string | Thumbnail URL | | ↳ `subreddit` | string | Subreddit name | +| `after` | string | Fullname of the last item for forward pagination | +| `before` | string | Fullname of the first item for backward pagination | ### `reddit_get_comments` @@ -83,12 +87,9 @@ Fetch comments from a specific Reddit post | `context` | number | No | Number of parent comments to include | | `showedits` | boolean | No | Show edit information for comments | | `showmore` | boolean | No | Include "load more comments" elements in the response | -| `showtitle` | boolean | No | Include submission title in the response | | `threaded` | boolean | No | Return comments in threaded/nested format | | `truncate` | number | No | Integer to truncate comment depth | -| `after` | string | No | Fullname of a thing to fetch items after \(for pagination\) | -| `before` | string | No | Fullname of a thing to fetch items before \(for pagination\) | -| `count` | number | No | A count of items already seen in the listing \(used for numbering\) | +| `comment` | string | No | ID36 of a comment to focus on \(returns that comment thread\) | #### Output @@ -96,6 +97,7 @@ Fetch comments from a specific Reddit post | --------- | ---- | ----------- | | `post` | object | Post information including ID, title, author, content, and metadata | | ↳ `id` | string | Post ID | +| ↳ `name` | string | Thing fullname \(t3_xxxxx\) | | ↳ `title` | string | Post title | | ↳ `author` | string | Post author | | ↳ `selftext` | string | Post text content | @@ -104,6 +106,7 @@ Fetch comments from a specific Reddit post | ↳ `permalink` | string | Reddit permalink | | `comments` | array | Nested comments with author, body, score, timestamps, and replies | | ↳ `id` | string | Comment ID | +| ↳ `name` | string | Thing fullname \(t1_xxxxx\) | | ↳ `author` | string | Comment author | | ↳ `body` | string | Comment text | | ↳ `score` | number | Comment score | @@ -135,6 +138,7 @@ Fetch controversial posts from a subreddit | `subreddit` | string | Name of the subreddit where posts were fetched from | | `posts` | array | Array of controversial posts with title, author, URL, score, comments count, and metadata | | ↳ `id` | string | Post ID | +| ↳ `name` | string | Thing fullname \(t3_xxxxx\) | | ↳ `title` | string | Post title | | ↳ `author` | string | Author username | | ↳ `url` | string | Post URL | @@ -146,6 +150,8 @@ Fetch controversial posts from a subreddit | ↳ `selftext` | string | Text content for self posts | | ↳ `thumbnail` | string | Thumbnail URL | | ↳ `subreddit` | string | Subreddit name | +| `after` | string | Fullname of the last item for forward pagination | +| `before` | string | Fullname of the first item for backward pagination | ### `reddit_search` @@ -165,6 +171,8 @@ Search for posts within a subreddit | `before` | string | No | Fullname of a thing to fetch items before \(for pagination\) | | `count` | number | No | A count of items already seen in the listing \(used for numbering\) | | `show` | string | No | Show items that would normally be filtered \(e.g., "all"\) | +| `type` | string | No | Type of search results: "link" \(posts\), "sr" \(subreddits\), or "user" \(users\). Default: "link" | +| `sr_detail` | boolean | No | Expand subreddit details in the response | #### Output @@ -173,6 +181,7 @@ Search for posts within a subreddit | `subreddit` | string | Name of the subreddit where search was performed | | `posts` | array | Array of search result posts with title, author, URL, score, comments count, and metadata | | ↳ `id` | string | Post ID | +| ↳ `name` | string | Thing fullname \(t3_xxxxx\) | | ↳ `title` | string | Post title | | ↳ `author` | string | Author username | | ↳ `url` | string | Post URL | @@ -184,6 +193,8 @@ Search for posts within a subreddit | ↳ `selftext` | string | Text content for self posts | | ↳ `thumbnail` | string | Thumbnail URL | | ↳ `subreddit` | string | Subreddit name | +| `after` | string | Fullname of the last item for forward pagination | +| `before` | string | Fullname of the first item for backward pagination | ### `reddit_submit_post` @@ -200,6 +211,9 @@ Submit a new post to a subreddit (text or link) | `nsfw` | boolean | No | Mark post as NSFW | | `spoiler` | boolean | No | Mark post as spoiler | | `send_replies` | boolean | No | Send reply notifications to inbox \(default: true\) | +| `flair_id` | string | No | Flair template UUID for the post \(max 36 characters\) | +| `flair_text` | string | No | Flair text to display on the post \(max 64 characters\) | +| `collection_id` | string | No | Collection UUID to add the post to | #### Output @@ -264,6 +278,21 @@ Save a Reddit post or comment to your saved items | `posts` | json | Posts data | | `post` | json | Single post data | | `comments` | json | Comments data | +| `success` | boolean | Operation success status | +| `message` | string | Result message | +| `data` | json | Response data | +| `after` | string | Pagination cursor \(next page\) | +| `before` | string | Pagination cursor \(previous page\) | +| `id` | string | Entity ID | +| `name` | string | Entity fullname | +| `messages` | json | Messages data | +| `display_name` | string | Subreddit display name | +| `subscribers` | number | Subscriber count | +| `description` | string | Description text | +| `link_karma` | number | Link karma | +| `comment_karma` | number | Comment karma | +| `total_karma` | number | Total karma | +| `icon_img` | string | Icon image URL | ### `reddit_reply` @@ -275,6 +304,7 @@ Add a comment reply to a Reddit post or comment | --------- | ---- | -------- | ----------- | | `parent_id` | string | Yes | Thing fullname to reply to \(e.g., "t3_abc123" for post, "t1_def456" for comment\) | | `text` | string | Yes | Comment text in markdown format \(e.g., "Great post! Here is my **reply**"\) | +| `return_rtjson` | boolean | No | Return response in Rich Text JSON format | #### Output @@ -345,4 +375,138 @@ Subscribe or unsubscribe from a subreddit | `success` | boolean | Whether the subscription action was successful | | `message` | string | Success or error message | +### `reddit_get_me` + +Get information about the authenticated Reddit user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | User ID | +| `name` | string | Username | +| `created_utc` | number | Account creation time in UTC epoch seconds | +| `link_karma` | number | Total link karma | +| `comment_karma` | number | Total comment karma | +| `total_karma` | number | Combined total karma | +| `is_gold` | boolean | Whether user has Reddit Premium | +| `is_mod` | boolean | Whether user is a moderator | +| `has_verified_email` | boolean | Whether email is verified | +| `icon_img` | string | User avatar/icon URL | + +### `reddit_get_user` + +Get public profile information about any Reddit user by username + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `username` | string | Yes | Reddit username to look up \(e.g., "spez", "example_user"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | User ID | +| `name` | string | Username | +| `created_utc` | number | Account creation time in UTC epoch seconds | +| `link_karma` | number | Total link karma | +| `comment_karma` | number | Total comment karma | +| `total_karma` | number | Combined total karma | +| `is_gold` | boolean | Whether user has Reddit Premium | +| `is_mod` | boolean | Whether user is a moderator | +| `has_verified_email` | boolean | Whether email is verified | +| `icon_img` | string | User avatar/icon URL | + +### `reddit_send_message` + +Send a private message to a Reddit user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `to` | string | Yes | Recipient username \(e.g., "example_user"\) or subreddit \(e.g., "/r/subreddit"\) | +| `subject` | string | Yes | Message subject \(max 100 characters\) | +| `text` | string | Yes | Message body in markdown format | +| `from_sr` | string | No | Subreddit name to send the message from \(requires moderator mail permission\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the message was sent successfully | +| `message` | string | Success or error message | + +### `reddit_get_messages` + +Retrieve private messages from your Reddit inbox + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `where` | string | No | Message folder to retrieve: "inbox" \(all\), "unread", "sent", "messages" \(direct messages only\), "comments" \(comment replies\), "selfreply" \(self-post replies\), or "mentions" \(username mentions\). Default: "inbox" | +| `limit` | number | No | Maximum number of messages to return \(e.g., 25\). Default: 25, max: 100 | +| `after` | string | No | Fullname of a thing to fetch items after \(for pagination\) | +| `before` | string | No | Fullname of a thing to fetch items before \(for pagination\) | +| `mark` | boolean | No | Whether to mark fetched messages as read | +| `count` | number | No | A count of items already seen in the listing \(used for numbering\) | +| `show` | string | No | Show items that would normally be filtered \(e.g., "all"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `messages` | array | Array of messages with sender, recipient, subject, body, and metadata | +| ↳ `id` | string | Message ID | +| ↳ `name` | string | Thing fullname \(t4_xxxxx\) | +| ↳ `author` | string | Sender username | +| ↳ `dest` | string | Recipient username | +| ↳ `subject` | string | Message subject | +| ↳ `body` | string | Message body text | +| ↳ `created_utc` | number | Creation time in UTC epoch seconds | +| ↳ `new` | boolean | Whether the message is unread | +| ↳ `was_comment` | boolean | Whether the message is a comment reply | +| ↳ `context` | string | Context URL for comment replies | +| ↳ `distinguished` | string | Distinction: null/"moderator"/"admin" | +| `after` | string | Fullname of the last item for forward pagination | +| `before` | string | Fullname of the first item for backward pagination | + +### `reddit_get_subreddit_info` + +Get metadata and information about a subreddit + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `subreddit` | string | Yes | The subreddit to get info about \(e.g., "technology", "programming", "news"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Subreddit ID | +| `name` | string | Subreddit fullname \(t5_xxxxx\) | +| `display_name` | string | Subreddit name without prefix | +| `title` | string | Subreddit title | +| `description` | string | Full subreddit description \(markdown\) | +| `public_description` | string | Short public description | +| `subscribers` | number | Number of subscribers | +| `accounts_active` | number | Number of currently active users | +| `created_utc` | number | Creation time in UTC epoch seconds | +| `over18` | boolean | Whether the subreddit is NSFW | +| `lang` | string | Primary language of the subreddit | +| `subreddit_type` | string | Subreddit type: public, private, restricted, etc. | +| `url` | string | Subreddit URL path \(e.g., /r/technology/\) | +| `icon_img` | string | Subreddit icon URL | +| `banner_img` | string | Subreddit banner URL | + diff --git a/apps/sim/blocks/blocks/reddit.ts b/apps/sim/blocks/blocks/reddit.ts index 83c0aa6761c..801e35b859e 100644 --- a/apps/sim/blocks/blocks/reddit.ts +++ b/apps/sim/blocks/blocks/reddit.ts @@ -138,7 +138,7 @@ export const RedditBlock: BlockConfig = { placeholder: 'Fullname for forward pagination (e.g., t3_xxxxx)', condition: { field: 'operation', - value: ['get_posts', 'get_controversial', 'search'], + value: ['get_posts', 'get_controversial', 'search', 'get_messages'], }, mode: 'advanced', }, @@ -149,7 +149,7 @@ export const RedditBlock: BlockConfig = { placeholder: 'Fullname for backward pagination (e.g., t3_xxxxx)', condition: { field: 'operation', - value: ['get_posts', 'get_controversial', 'search'], + value: ['get_posts', 'get_controversial', 'search', 'get_messages'], }, mode: 'advanced', }, @@ -818,6 +818,8 @@ Return ONLY the message content - no meta-commentary.`, where: inputs.messageWhere, limit: inputs.messageLimit ? Number.parseInt(inputs.messageLimit) : undefined, mark: inputs.messageMark !== undefined ? inputs.messageMark === 'true' : undefined, + after: inputs.after || undefined, + before: inputs.before || undefined, oauthCredential, } } diff --git a/apps/sim/tools/reddit/get_comments.ts b/apps/sim/tools/reddit/get_comments.ts index c879ea2c503..36208a66fe6 100644 --- a/apps/sim/tools/reddit/get_comments.ts +++ b/apps/sim/tools/reddit/get_comments.ts @@ -93,7 +93,7 @@ export const getCommentsTool: ToolConfig { const subreddit = normalizeSubreddit(params.subreddit) const sort = params.sort || 'confidence' - const limit = Math.min(Math.max(1, params.limit || 50), 100) + const limit = Math.min(Math.max(1, params.limit ?? 50), 100) // Build URL with query parameters const urlParams = new URLSearchParams({ @@ -115,7 +115,7 @@ export const getCommentsTool: ToolConfig { @@ -135,7 +135,7 @@ export const getCommentsTool: ToolConfig = // Extract subreddit name from response (with fallback) const subredditName = - data.data?.children[0]?.data?.subreddit || requestParams?.subreddit || 'unknown' + data.data?.children?.[0]?.data?.subreddit || requestParams?.subreddit || 'unknown' // Transform posts data const posts = diff --git a/apps/sim/tools/reddit/search.ts b/apps/sim/tools/reddit/search.ts index bbac78b0ea1..4e51c2aa5ca 100644 --- a/apps/sim/tools/reddit/search.ts +++ b/apps/sim/tools/reddit/search.ts @@ -148,7 +148,7 @@ export const searchTool: ToolConfig = { // Extract subreddit name from response (with fallback) const subredditName = - data.data?.children[0]?.data?.subreddit || requestParams?.subreddit || 'unknown' + data.data?.children?.[0]?.data?.subreddit || requestParams?.subreddit || 'unknown' // Transform posts data const posts = From e751d6c49f09236337e88869fef56c5ce3dc76c3 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 19:35:11 -0800 Subject: [PATCH 3/5] chore(reddit): remove unused output property constants from types.ts --- apps/sim/tools/reddit/types.ts | 77 ---------------------------------- 1 file changed, 77 deletions(-) diff --git a/apps/sim/tools/reddit/types.ts b/apps/sim/tools/reddit/types.ts index 28395fb7031..3573ea80607 100644 --- a/apps/sim/tools/reddit/types.ts +++ b/apps/sim/tools/reddit/types.ts @@ -242,83 +242,6 @@ export const EDIT_DATA_OUTPUT: OutputProperty = { properties: EDIT_DATA_OUTPUT_PROPERTIES, } -/** - * Pagination cursor output properties for listing responses - */ -export const PAGINATION_OUTPUT_PROPERTIES = { - after: { - type: 'string', - description: 'Fullname of the last item for forward pagination', - optional: true, - }, - before: { - type: 'string', - description: 'Fullname of the first item for backward pagination', - optional: true, - }, -} as const satisfies Record - -/** - * User profile output properties - */ -export const USER_PROFILE_OUTPUT_PROPERTIES = { - id: { type: 'string', description: 'User ID' }, - name: { type: 'string', description: 'Username' }, - created_utc: { type: 'number', description: 'Account creation time in UTC epoch seconds' }, - link_karma: { type: 'number', description: 'Total link karma' }, - comment_karma: { type: 'number', description: 'Total comment karma' }, - total_karma: { type: 'number', description: 'Combined total karma' }, - is_gold: { type: 'boolean', description: 'Whether user has Reddit Premium' }, - is_mod: { type: 'boolean', description: 'Whether user is a moderator' }, - has_verified_email: { type: 'boolean', description: 'Whether email is verified' }, - icon_img: { type: 'string', description: 'User avatar/icon URL' }, -} as const satisfies Record - -/** - * Message output properties - */ -export const MESSAGE_OUTPUT_PROPERTIES = { - id: { type: 'string', description: 'Message ID' }, - name: { type: 'string', description: 'Thing fullname (t4_xxxxx)' }, - author: { type: 'string', description: 'Sender username' }, - dest: { type: 'string', description: 'Recipient username' }, - subject: { type: 'string', description: 'Message subject' }, - body: { type: 'string', description: 'Message body text' }, - created_utc: { type: 'number', description: 'Creation time in UTC epoch seconds' }, - new: { type: 'boolean', description: 'Whether the message is unread' }, - was_comment: { type: 'boolean', description: 'Whether the message is a comment reply' }, - context: { type: 'string', description: 'Context URL for comment replies' }, - distinguished: { - type: 'string', - description: 'Distinction: null/"moderator"/"admin"', - optional: true, - }, -} as const satisfies Record - -/** - * Subreddit info output properties - */ -export const SUBREDDIT_INFO_OUTPUT_PROPERTIES = { - id: { type: 'string', description: 'Subreddit ID' }, - name: { type: 'string', description: 'Subreddit fullname (t5_xxxxx)' }, - display_name: { type: 'string', description: 'Subreddit name without prefix' }, - title: { type: 'string', description: 'Subreddit title' }, - description: { type: 'string', description: 'Full subreddit description (markdown)' }, - public_description: { type: 'string', description: 'Short public description' }, - subscribers: { type: 'number', description: 'Number of subscribers' }, - accounts_active: { type: 'number', description: 'Number of currently active users' }, - created_utc: { type: 'number', description: 'Creation time in UTC epoch seconds' }, - over18: { type: 'boolean', description: 'Whether the subreddit is NSFW' }, - lang: { type: 'string', description: 'Primary language of the subreddit' }, - subreddit_type: { - type: 'string', - description: 'Subreddit type: public, private, restricted, etc.', - }, - url: { type: 'string', description: 'Subreddit URL path (e.g., /r/technology/)' }, - icon_img: { type: 'string', description: 'Subreddit icon URL', optional: true }, - banner_img: { type: 'string', description: 'Subreddit banner URL', optional: true }, -} as const satisfies Record - export interface RedditPost { id: string name: string From a5dbd9678bdbd3f42a71f9796740dcb3df5e115d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 19:37:24 -0800 Subject: [PATCH 4/5] fix(reddit): add HTTP error handling to GET tools Add !response.ok guards to get_me, get_user, get_subreddit_info, and get_messages to return success: false on non-2xx responses instead of silently returning empty data with success: true. --- apps/sim/tools/reddit/get_me.ts | 18 ++++++++++++++++ apps/sim/tools/reddit/get_messages.ts | 7 ++++++ apps/sim/tools/reddit/get_subreddit_info.ts | 24 +++++++++++++++++++++ apps/sim/tools/reddit/get_user.ts | 19 ++++++++++++++++ 4 files changed, 68 insertions(+) diff --git a/apps/sim/tools/reddit/get_me.ts b/apps/sim/tools/reddit/get_me.ts index b06afd14504..3f3336c452e 100644 --- a/apps/sim/tools/reddit/get_me.ts +++ b/apps/sim/tools/reddit/get_me.ts @@ -40,6 +40,24 @@ export const getMeTool: ToolConfig = { transformResponse: async (response: Response) => { const data = await response.json() + if (!response.ok) { + return { + success: false, + output: { + id: '', + name: '', + created_utc: 0, + link_karma: 0, + comment_karma: 0, + total_karma: 0, + is_gold: false, + is_mod: false, + has_verified_email: false, + icon_img: '', + }, + } + } + return { success: true, output: { diff --git a/apps/sim/tools/reddit/get_messages.ts b/apps/sim/tools/reddit/get_messages.ts index da7c4e2ff4f..c26130121f0 100644 --- a/apps/sim/tools/reddit/get_messages.ts +++ b/apps/sim/tools/reddit/get_messages.ts @@ -99,6 +99,13 @@ export const getMessagesTool: ToolConfig { const data = await response.json() + if (!response.ok) { + return { + success: false, + output: { messages: [], after: null, before: null }, + } + } + const messages = data.data?.children?.map((child: any) => { const msg = child.data || {} diff --git a/apps/sim/tools/reddit/get_subreddit_info.ts b/apps/sim/tools/reddit/get_subreddit_info.ts index ae81f9c57d5..17d62b2e7be 100644 --- a/apps/sim/tools/reddit/get_subreddit_info.ts +++ b/apps/sim/tools/reddit/get_subreddit_info.ts @@ -55,6 +55,30 @@ export const getSubredditInfoTool: ToolConfig< transformResponse: async (response: Response) => { const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { + id: '', + name: '', + display_name: '', + title: '', + description: '', + public_description: '', + subscribers: 0, + accounts_active: 0, + created_utc: 0, + over18: false, + lang: '', + subreddit_type: '', + url: '', + icon_img: null, + banner_img: null, + }, + } + } + const sub = data.data || data return { diff --git a/apps/sim/tools/reddit/get_user.ts b/apps/sim/tools/reddit/get_user.ts index 91a68592fc6..e30293c9954 100644 --- a/apps/sim/tools/reddit/get_user.ts +++ b/apps/sim/tools/reddit/get_user.ts @@ -48,6 +48,25 @@ export const getUserTool: ToolConfig = transformResponse: async (response: Response) => { const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { + id: '', + name: '', + created_utc: 0, + link_karma: 0, + comment_karma: 0, + total_karma: 0, + is_gold: false, + is_mod: false, + has_verified_email: false, + icon_img: '', + }, + } + } + const user = data.data || data return { From 0f6b08dbcff7146933a26b3891a79236e60c72bc Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 19:47:43 -0800 Subject: [PATCH 5/5] fix(reddit): add input validation and HTTP error guards - Add validateEnum/validatePathSegment to prevent URL path traversal - Add !response.ok guards to send_message and reply tools - Centralize subreddit validation in normalizeSubreddit --- apps/sim/tools/reddit/get_comments.ts | 10 +++++++++- apps/sim/tools/reddit/get_messages.ts | 15 +++++++++++++++ apps/sim/tools/reddit/get_posts.ts | 7 +++++++ apps/sim/tools/reddit/get_user.ts | 5 +++++ apps/sim/tools/reddit/reply.ts | 11 +++++++++++ apps/sim/tools/reddit/send_message.ts | 11 +++++++++++ apps/sim/tools/reddit/utils.ts | 11 ++++++++++- 7 files changed, 68 insertions(+), 2 deletions(-) diff --git a/apps/sim/tools/reddit/get_comments.ts b/apps/sim/tools/reddit/get_comments.ts index 36208a66fe6..b22366cdf17 100644 --- a/apps/sim/tools/reddit/get_comments.ts +++ b/apps/sim/tools/reddit/get_comments.ts @@ -1,3 +1,4 @@ +import { validatePathSegment } from '@/lib/core/security/input-validation' import type { RedditCommentsParams, RedditCommentsResponse } from '@/tools/reddit/types' import { normalizeSubreddit } from '@/tools/reddit/utils' import type { ToolConfig } from '@/tools/types' @@ -114,8 +115,15 @@ export const getCommentsTool: ToolConfig { diff --git a/apps/sim/tools/reddit/get_messages.ts b/apps/sim/tools/reddit/get_messages.ts index c26130121f0..c0dae039480 100644 --- a/apps/sim/tools/reddit/get_messages.ts +++ b/apps/sim/tools/reddit/get_messages.ts @@ -1,6 +1,17 @@ +import { validateEnum } from '@/lib/core/security/input-validation' import type { RedditGetMessagesParams, RedditMessagesResponse } from '@/tools/reddit/types' import type { ToolConfig } from '@/tools/types' +const ALLOWED_MESSAGE_FOLDERS = [ + 'inbox', + 'unread', + 'sent', + 'messages', + 'comments', + 'selfreply', + 'mentions', +] as const + export const getMessagesTool: ToolConfig = { id: 'reddit_get_messages', name: 'Get Reddit Messages', @@ -67,6 +78,10 @@ export const getMessagesTool: ToolConfig { const where = params.where || 'inbox' + const validation = validateEnum(where, ALLOWED_MESSAGE_FOLDERS, 'where') + if (!validation.isValid) { + throw new Error(validation.error) + } const limit = Math.min(Math.max(1, params.limit ?? 25), 100) const urlParams = new URLSearchParams({ diff --git a/apps/sim/tools/reddit/get_posts.ts b/apps/sim/tools/reddit/get_posts.ts index 86ff0a7e4b1..b3e9f783e4f 100644 --- a/apps/sim/tools/reddit/get_posts.ts +++ b/apps/sim/tools/reddit/get_posts.ts @@ -1,7 +1,10 @@ +import { validateEnum } from '@/lib/core/security/input-validation' import type { RedditPostsParams, RedditPostsResponse } from '@/tools/reddit/types' import { normalizeSubreddit } from '@/tools/reddit/utils' import type { ToolConfig } from '@/tools/types' +const ALLOWED_SORT_OPTIONS = ['hot', 'new', 'top', 'controversial', 'rising'] as const + export const getPostsTool: ToolConfig = { id: 'reddit_get_posts', name: 'Get Reddit Posts', @@ -88,6 +91,10 @@ export const getPostsTool: ToolConfig = url: (params: RedditPostsParams) => { const subreddit = normalizeSubreddit(params.subreddit) const sort = params.sort || 'hot' + const sortValidation = validateEnum(sort, ALLOWED_SORT_OPTIONS, 'sort') + if (!sortValidation.isValid) { + throw new Error(sortValidation.error) + } const limit = Math.min(Math.max(1, params.limit ?? 10), 100) // Build URL with appropriate parameters using OAuth endpoint diff --git a/apps/sim/tools/reddit/get_user.ts b/apps/sim/tools/reddit/get_user.ts index e30293c9954..cb14897b091 100644 --- a/apps/sim/tools/reddit/get_user.ts +++ b/apps/sim/tools/reddit/get_user.ts @@ -1,3 +1,4 @@ +import { validatePathSegment } from '@/lib/core/security/input-validation' import type { RedditGetUserParams, RedditUserResponse } from '@/tools/reddit/types' import type { ToolConfig } from '@/tools/types' @@ -30,6 +31,10 @@ export const getUserTool: ToolConfig = request: { url: (params: RedditGetUserParams) => { const username = params.username.trim().replace(/^u\//, '') + const validation = validatePathSegment(username, { paramName: 'username' }) + if (!validation.isValid) { + throw new Error(validation.error) + } return `https://oauth.reddit.com/user/${username}/about?raw_json=1` }, method: 'GET', diff --git a/apps/sim/tools/reddit/reply.ts b/apps/sim/tools/reddit/reply.ts index 241a08da174..7a29af3aa91 100644 --- a/apps/sim/tools/reddit/reply.ts +++ b/apps/sim/tools/reddit/reply.ts @@ -71,6 +71,17 @@ export const replyTool: ToolConfig = { transformResponse: async (response: Response) => { const data = await response.json() + if (!response.ok) { + const errorMsg = data?.message || `HTTP error ${response.status}` + return { + success: false, + output: { + success: false, + message: `Failed to post reply: ${errorMsg}`, + }, + } + } + // Reddit API returns errors in json.errors array if (data.json?.errors && data.json.errors.length > 0) { const errors = data.json.errors.map((err: any) => err.join(': ')).join(', ') diff --git a/apps/sim/tools/reddit/send_message.ts b/apps/sim/tools/reddit/send_message.ts index d53c88f52c6..0c20330b9c1 100644 --- a/apps/sim/tools/reddit/send_message.ts +++ b/apps/sim/tools/reddit/send_message.ts @@ -78,6 +78,17 @@ export const sendMessageTool: ToolConfig { const data = await response.json() + if (!response.ok) { + const errorMsg = data?.message || `HTTP error ${response.status}` + return { + success: false, + output: { + success: false, + message: `Failed to send message: ${errorMsg}`, + }, + } + } + if (data.json?.errors && data.json.errors.length > 0) { const errors = data.json.errors.map((err: any) => err.join(': ')).join(', ') return { diff --git a/apps/sim/tools/reddit/utils.ts b/apps/sim/tools/reddit/utils.ts index 6a7f9b2736b..be88740d8f9 100644 --- a/apps/sim/tools/reddit/utils.ts +++ b/apps/sim/tools/reddit/utils.ts @@ -1,10 +1,19 @@ +import { validatePathSegment } from '@/lib/core/security/input-validation' + const SUBREDDIT_PREFIX = /^r\// /** * Normalizes a subreddit name by removing the 'r/' prefix if present and trimming whitespace. + * Validates the result to prevent path traversal attacks. * @param subreddit - The subreddit name to normalize * @returns The normalized subreddit name without the 'r/' prefix + * @throws Error if the subreddit name contains invalid characters */ export function normalizeSubreddit(subreddit: string): string { - return subreddit.trim().replace(SUBREDDIT_PREFIX, '') + const normalized = subreddit.trim().replace(SUBREDDIT_PREFIX, '') + const validation = validatePathSegment(normalized, { paramName: 'subreddit' }) + if (!validation.isValid) { + throw new Error(validation.error) + } + return normalized }