chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
import type { RedditDeleteParams, RedditWriteResponse } from '@/tools/reddit/types'
import type { ToolConfig } from '@/tools/types'
export const deleteTool: ToolConfig<RedditDeleteParams, RedditWriteResponse> = {
id: 'reddit_delete',
name: 'Delete Reddit Post/Comment',
description: 'Delete your own Reddit post or comment',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Thing fullname to delete (e.g., "t3_abc123" for post, "t1_def456" for comment)',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/del',
method: 'POST',
headers: (params: RedditDeleteParams) => {
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: RedditDeleteParams) => {
const formData = new URLSearchParams({
id: params.id,
})
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditDeleteParams) => {
await response.json().catch(() => ({}))
if (response.ok) {
return {
success: true,
output: {
success: true,
message: `Successfully deleted ${requestParams?.id}`,
},
}
}
return {
success: false,
output: {
success: false,
message: 'Failed to delete item',
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the deletion was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
+127
View File
@@ -0,0 +1,127 @@
import type { RedditEditParams, RedditWriteResponse } from '@/tools/reddit/types'
import type { ToolConfig } from '@/tools/types'
export const editTool: ToolConfig<RedditEditParams, RedditWriteResponse> = {
id: 'reddit_edit',
name: 'Edit Reddit Post/Comment',
description: 'Edit the text of your own Reddit post or comment',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
thing_id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Thing fullname to edit (e.g., "t3_abc123" for post, "t1_def456" for comment)',
},
text: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'New text content in markdown format (e.g., "Updated **content** here")',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/editusertext',
method: 'POST',
headers: (params: RedditEditParams) => {
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: RedditEditParams) => {
const formData = new URLSearchParams({
thing_id: params.thing_id,
text: params.text,
api_type: 'json',
})
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditEditParams) => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
output: {
success: false,
message: `Failed to edit: HTTP error ${response.status}`,
},
}
}
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 edit: ${errors}`,
},
}
}
const thingData = data.json?.data?.things?.[0]?.data
return {
success: true,
output: {
success: true,
message: `Successfully edited ${requestParams?.thing_id}`,
data: {
id: thingData?.id,
body: thingData?.body,
selftext: thingData?.selftext,
},
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the edit was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
data: {
type: 'object',
description: 'Updated content data',
properties: {
id: { type: 'string', description: 'Edited thing ID' },
body: {
type: 'string',
description: 'Updated comment body (for comments)',
optional: true,
},
selftext: {
type: 'string',
description: 'Updated post text (for self posts)',
optional: true,
},
},
},
},
}
+231
View File
@@ -0,0 +1,231 @@
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'
export const getCommentsTool: ToolConfig<RedditCommentsParams, RedditCommentsResponse> = {
id: 'reddit_get_comments',
name: 'Get Reddit Comments',
description: 'Fetch comments from a specific Reddit post',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
postId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the Reddit post to fetch comments from (e.g., "abc123")',
},
subreddit: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The subreddit where the post is located (e.g., "technology", "programming")',
},
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Sort method for comments: "confidence", "top", "new", "controversial", "old", "random", "qa" (default: "confidence")',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of comments to return (e.g., 25). Default: 50, max: 100',
},
depth: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum depth of subtrees in the thread (controls nested comment levels)',
},
context: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of parent comments to include',
},
showedits: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Show edit information for comments',
},
showmore: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Include "load more comments" elements in the response',
},
threaded: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Return comments in threaded/nested format',
},
truncate: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Integer to truncate comment depth',
},
comment: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'ID36 of a comment to focus on (returns that comment thread)',
},
},
request: {
url: (params: RedditCommentsParams) => {
const subreddit = normalizeSubreddit(params.subreddit)
const sort = params.sort || 'confidence'
const limit = Math.min(Math.max(1, params.limit ?? 50), 100)
const urlParams = new URLSearchParams({
sort: sort,
limit: limit.toString(),
raw_json: '1',
})
if (params.depth !== undefined) urlParams.append('depth', Number(params.depth).toString())
if (params.context !== undefined)
urlParams.append('context', Number(params.context).toString())
if (params.showedits !== undefined) urlParams.append('showedits', params.showedits.toString())
if (params.showmore !== undefined) urlParams.append('showmore', params.showmore.toString())
if (params.threaded !== undefined) urlParams.append('threaded', params.threaded.toString())
if (params.truncate !== undefined)
urlParams.append('truncate', Number(params.truncate).toString())
if (params.comment) urlParams.append('comment', params.comment)
const postId = params.postId.trim()
const postIdValidation = validatePathSegment(postId, { paramName: 'postId' })
if (!postIdValidation.isValid) {
throw new Error(postIdValidation.error)
}
return `https://oauth.reddit.com/r/${subreddit}/comments/${postId}?${urlParams.toString()}`
},
method: 'GET',
headers: (params: RedditCommentsParams) => {
if (!params.accessToken?.trim()) {
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, requestParams?: RedditCommentsParams) => {
const data = await response.json()
const postData = data[0]?.data?.children?.[0]?.data || {}
const commentsData = data[1]?.data?.children || []
const processComments = (comments: any[]): any[] => {
return comments
.map((comment) => {
const commentData = comment.data
if (!commentData || comment.kind !== 't1') {
return null
}
const replies = commentData.replies?.data?.children
? processComments(commentData.replies.data.children)
: []
return {
id: commentData.id ?? '',
name: commentData.name ?? '',
author: commentData.author || '[deleted]',
body: commentData.body ?? '',
created_utc: commentData.created_utc ?? 0,
score: commentData.score ?? 0,
permalink: commentData.permalink
? `https://www.reddit.com${commentData.permalink}`
: '',
replies: replies.filter(Boolean),
}
})
.filter(Boolean)
}
const comments = processComments(commentsData)
return {
success: true,
output: {
post: {
id: postData.id ?? '',
name: postData.name ?? '',
title: postData.title ?? '',
author: postData.author || '[deleted]',
selftext: postData.selftext ?? '',
created_utc: postData.created_utc ?? 0,
score: postData.score ?? 0,
permalink: postData.permalink ? `https://www.reddit.com${postData.permalink}` : '',
},
comments: comments,
},
}
},
outputs: {
post: {
type: 'object',
description: 'Post information including ID, title, author, content, and metadata',
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' },
score: { type: 'number', description: 'Post score' },
created_utc: { type: 'number', description: 'Creation timestamp' },
permalink: { type: 'string', description: 'Reddit permalink' },
},
},
comments: {
type: 'array',
description: 'Nested comments with author, body, score, timestamps, and replies',
items: {
type: 'object',
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' },
created_utc: { type: 'number', description: 'Creation timestamp' },
permalink: { type: 'string', description: 'Comment permalink' },
replies: {
type: 'array',
description: 'Nested reply comments',
items: { type: 'object', description: 'Nested comment with same structure' },
},
},
},
},
},
}
+186
View File
@@ -0,0 +1,186 @@
import type { RedditControversialParams, RedditPostsResponse } from '@/tools/reddit/types'
import { normalizeSubreddit } from '@/tools/reddit/utils'
import type { ToolConfig } from '@/tools/types'
export const getControversialTool: ToolConfig<RedditControversialParams, RedditPostsResponse> = {
id: 'reddit_get_controversial',
name: 'Get Reddit Controversial Posts',
description: 'Fetch controversial posts from 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 fetch posts from (e.g., "technology", "news")',
},
time: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Time filter for controversial posts: "hour", "day", "week", "month", "year", or "all" (default: "all")',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of posts to return (e.g., 25). Default: 10, 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)',
},
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")',
},
sr_detail: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Expand subreddit details in the response',
},
},
request: {
url: (params: RedditControversialParams) => {
const subreddit = normalizeSubreddit(params.subreddit)
const limit = Math.min(Math.max(1, params.limit ?? 10), 100)
const urlParams = new URLSearchParams({
limit: limit.toString(),
raw_json: '1',
})
if (params.time) {
urlParams.append('t', params.time)
}
if (params.after) urlParams.append('after', params.after)
if (params.before) urlParams.append('before', params.before)
if (params.count !== undefined) urlParams.append('count', params.count.toString())
if (params.show) urlParams.append('show', params.show)
if (params.sr_detail !== undefined) urlParams.append('sr_detail', params.sr_detail.toString())
return `https://oauth.reddit.com/r/${subreddit}/controversial?${urlParams.toString()}`
},
method: 'GET',
headers: (params: RedditControversialParams) => {
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, requestParams?: RedditControversialParams) => {
const data = await response.json()
const subredditName =
data.data?.children?.[0]?.data?.subreddit || requestParams?.subreddit || 'unknown'
const posts =
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,
is_self: !!post.is_self,
selftext: post.selftext ?? '',
thumbnail: post.thumbnail ?? '',
subreddit: post.subreddit ?? subredditName,
}
}) || []
return {
success: true,
output: {
subreddit: subredditName,
posts,
after: data.data?.after ?? null,
before: data.data?.before ?? null,
},
}
},
outputs: {
subreddit: {
type: 'string',
description: 'Name of the subreddit where posts were fetched from',
},
posts: {
type: 'array',
description:
'Array of controversial posts with title, author, URL, score, comments count, and metadata',
items: {
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' },
permalink: { type: 'string', description: 'Reddit permalink' },
score: { type: 'number', description: 'Post score (upvotes - downvotes)' },
num_comments: { type: 'number', description: 'Number of comments' },
created_utc: { type: 'number', description: 'Creation timestamp (UTC)' },
is_self: { type: 'boolean', description: 'Whether this is a text post' },
selftext: { type: 'string', description: 'Text content for self posts' },
thumbnail: { type: 'string', description: 'Thumbnail URL' },
subreddit: { type: 'string', description: 'Subreddit name' },
},
},
},
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,
},
},
}
+221
View File
@@ -0,0 +1,221 @@
import {
COMMENT_LISTING_OUTPUT_PROPERTIES,
POST_LISTING_OUTPUT_PROPERTIES,
type RedditComment,
type RedditPost,
} from '@/tools/reddit/types'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface RedditInfoSubreddit {
id: string
name: string
display_name: string
title: string
public_description: string
subscribers: number
over18: boolean
url: string
subreddit_type: string
icon_img: string | null
created_utc: number
accounts_active: number
}
interface RedditGetInfoParams {
id: string
accessToken?: string
}
interface RedditGetInfoResponse extends ToolResponse {
output: {
posts: RedditPost[]
comments: RedditComment[]
subreddits: RedditInfoSubreddit[]
}
}
export const getInfoTool: ToolConfig<RedditGetInfoParams, RedditGetInfoResponse> = {
id: 'reddit_get_info',
name: 'Get Reddit Info',
description:
'Fetch information about one or more Reddit things (posts, comments, or subreddits) by their fullnames',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Comma-separated list of thing fullnames to look up (e.g., "t3_abc123,t1_xyz789,t5_2qh33"). Prefixes: t1_ = comment, t3_ = post, t5_ = subreddit',
},
},
request: {
url: (params: RedditGetInfoParams) => {
const id = (params.id ?? '').trim()
if (!id) {
throw new Error('At least one thing fullname is required for Reddit API')
}
const urlParams = new URLSearchParams({
id,
raw_json: '1',
})
return `https://oauth.reddit.com/api/info?${urlParams.toString()}`
},
method: 'GET',
headers: (params: RedditGetInfoParams) => {
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()
if (!response.ok) {
return {
success: false,
output: {
posts: [],
comments: [],
subreddits: [],
},
}
}
const children = data.data?.children ?? []
const posts: RedditPost[] = children
.filter((child: any) => child.kind === 't3')
.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,
is_self: !!post.is_self,
selftext: post.selftext ?? '',
thumbnail: post.thumbnail ?? '',
subreddit: post.subreddit ?? '',
}
})
const comments: RedditComment[] = children
.filter((child: any) => child.kind === 't1')
.map((child: any) => {
const comment = child.data || {}
return {
id: comment.id ?? '',
name: comment.name ?? '',
author: comment.author || '[deleted]',
body: comment.body ?? '',
created_utc: comment.created_utc ?? 0,
score: comment.score ?? 0,
permalink: comment.permalink ? `https://www.reddit.com${comment.permalink}` : '',
replies: [],
}
})
const subreddits: RedditInfoSubreddit[] = children
.filter((child: any) => child.kind === 't5')
.map((child: any) => {
const sub = child.data || {}
return {
id: sub.id ?? '',
name: sub.name ?? '',
display_name: sub.display_name ?? '',
title: sub.title ?? '',
public_description: sub.public_description ?? '',
subscribers: sub.subscribers ?? 0,
over18: sub.over18 ?? false,
url: sub.url ?? '',
subreddit_type: sub.subreddit_type ?? '',
icon_img: sub.icon_img ?? null,
created_utc: sub.created_utc ?? 0,
accounts_active: sub.active_user_count ?? sub.accounts_active ?? 0,
}
})
return {
success: true,
output: {
posts,
comments,
subreddits,
},
}
},
outputs: {
posts: {
type: 'array',
description: 'Posts (t3) matched by the requested fullnames',
items: {
type: 'object',
properties: POST_LISTING_OUTPUT_PROPERTIES,
},
},
comments: {
type: 'array',
description: 'Comments (t1) matched by the requested fullnames',
items: {
type: 'object',
properties: COMMENT_LISTING_OUTPUT_PROPERTIES,
},
},
subreddits: {
type: 'array',
description: 'Subreddits (t5) matched by the requested fullnames',
items: {
type: 'object',
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' },
public_description: { type: 'string', description: 'Short public description' },
subscribers: { type: 'number', description: 'Number of subscribers' },
over18: { type: 'boolean', description: 'Whether the subreddit is NSFW' },
url: { type: 'string', description: 'Subreddit URL path (e.g., /r/technology/)' },
subreddit_type: {
type: 'string',
description: 'Subreddit type: public, private, restricted, etc.',
},
icon_img: { type: 'string', description: 'Subreddit icon URL', optional: true },
created_utc: { type: 'number', description: 'Creation time in UTC epoch seconds' },
accounts_active: {
type: 'number',
description: 'Number of currently active users',
},
},
},
},
},
}
+90
View File
@@ -0,0 +1,90 @@
import type { RedditGetMeParams, RedditUserResponse } from '@/tools/reddit/types'
import type { ToolConfig } from '@/tools/types'
export const getMeTool: ToolConfig<RedditGetMeParams, RedditUserResponse> = {
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()
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: {
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' },
},
}
+188
View File
@@ -0,0 +1,188 @@
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<RedditGetMessagesParams, RedditMessagesResponse> = {
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 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({
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()
if (!response.ok) {
return {
success: false,
output: { messages: [], after: null, before: null },
}
}
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,
},
},
}
+216
View File
@@ -0,0 +1,216 @@
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<RedditPostsParams, RedditPostsResponse> = {
id: 'reddit_get_posts',
name: 'Get Reddit Posts',
description: 'Fetch posts from a subreddit with different sorting options',
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 fetch posts from (e.g., "technology", "news")',
},
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Sort method for posts (e.g., "hot", "new", "top", "rising", "controversial"). Default: "hot"',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of posts to return (e.g., 25). Default: 10, max: 100',
},
time: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Time filter for "top" sorted posts: "day", "week", "month", "year", or "all" (default: "all")',
},
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)',
},
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")',
},
sr_detail: {
type: 'boolean',
required: false,
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 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)
const urlParams = new URLSearchParams({
limit: limit.toString(),
raw_json: '1',
})
if (
(sort === 'top' || sort === 'controversial') &&
params.time !== undefined &&
params.time !== null
) {
urlParams.append('t', params.time)
}
if (params.after !== undefined && params.after !== null && params.after !== '')
urlParams.append('after', params.after)
if (params.before !== undefined && params.before !== null && params.before !== '')
urlParams.append('before', params.before)
if (params.count !== undefined && params.count !== null)
urlParams.append('count', params.count.toString())
if (params.show !== undefined && params.show !== null && params.show !== '')
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()}`
},
method: 'GET',
headers: (params: RedditPostsParams) => {
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, requestParams?: RedditPostsParams) => {
const data = await response.json()
const subredditName =
data.data?.children?.[0]?.data?.subreddit || requestParams?.subreddit || 'unknown'
const posts =
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,
is_self: !!post.is_self,
selftext: post.selftext ?? '',
thumbnail: post.thumbnail ?? '',
subreddit: post.subreddit ?? subredditName,
}
}) || []
return {
success: true,
output: {
subreddit: subredditName,
posts,
after: data.data?.after ?? null,
before: data.data?.before ?? null,
},
}
},
outputs: {
subreddit: {
type: 'string',
description: 'Name of the subreddit where posts were fetched from',
},
posts: {
type: 'array',
description: 'Array of posts with title, author, URL, score, comments count, and metadata',
items: {
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' },
permalink: { type: 'string', description: 'Reddit permalink' },
score: { type: 'number', description: 'Post score (upvotes - downvotes)' },
num_comments: { type: 'number', description: 'Number of comments' },
created_utc: { type: 'number', description: 'Creation timestamp (UTC)' },
is_self: { type: 'boolean', description: 'Whether this is a text post' },
selftext: { type: 'string', description: 'Text content for self posts' },
thumbnail: { type: 'string', description: 'Thumbnail URL' },
subreddit: { type: 'string', description: 'Subreddit name' },
},
},
},
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,
},
},
}
+226
View File
@@ -0,0 +1,226 @@
import { validatePathSegment } from '@/lib/core/security/input-validation'
import {
COMMENT_LISTING_OUTPUT_PROPERTIES,
POST_LISTING_OUTPUT_PROPERTIES,
type RedditComment,
type RedditPost,
} from '@/tools/reddit/types'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface GetSavedParams {
username: string
limit?: number
after?: string
before?: string
count?: number
show?: string
sr_detail?: boolean
accessToken: string
}
interface GetSavedResponse extends ToolResponse {
output: {
posts: RedditPost[]
comments: RedditComment[]
after: string | null
before: string | null
}
}
export const getSavedTool: ToolConfig<GetSavedParams, GetSavedResponse> = {
id: 'reddit_get_saved',
name: 'Get Reddit Saved Items',
description:
'Fetch your own saved posts (t3) and comments (t1). You can only read your own saved items',
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:
'Your own Reddit username (saved items can only be read for the authenticated user)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of items 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)',
},
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")',
},
sr_detail: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Expand subreddit details in the response',
},
},
request: {
url: (params: GetSavedParams) => {
const username = params.username.trim().replace(/^u\//, '')
const validation = validatePathSegment(username, { paramName: 'username' })
if (!validation.isValid) {
throw new Error(validation.error)
}
const limit = Math.min(Math.max(1, params.limit ?? 25), 100)
const urlParams = new URLSearchParams({
limit: limit.toString(),
raw_json: '1',
})
if (params.after !== undefined && params.after !== null && params.after !== '')
urlParams.append('after', params.after)
if (params.before !== undefined && params.before !== null && params.before !== '')
urlParams.append('before', params.before)
if (params.count !== undefined && params.count !== null)
urlParams.append('count', params.count.toString())
if (params.show !== undefined && params.show !== null && params.show !== '')
urlParams.append('show', params.show)
if (params.sr_detail !== undefined && params.sr_detail !== null)
urlParams.append('sr_detail', params.sr_detail.toString())
return `https://oauth.reddit.com/user/${username}/saved?${urlParams.toString()}`
},
method: 'GET',
headers: (params: GetSavedParams) => {
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()
if (!response.ok) {
return {
success: false,
output: {
posts: [],
comments: [],
after: null,
before: null,
},
}
}
const posts: RedditPost[] = []
const comments: RedditComment[] = []
const children = data.data?.children || []
for (const child of children) {
const item = child.data || {}
if (child.kind === 't3') {
posts.push({
id: item.id ?? '',
name: item.name ?? '',
title: item.title ?? '',
author: item.author || '[deleted]',
url: item.url ?? '',
permalink: item.permalink ? `https://www.reddit.com${item.permalink}` : '',
created_utc: item.created_utc ?? 0,
score: item.score ?? 0,
num_comments: item.num_comments ?? 0,
is_self: !!item.is_self,
selftext: item.selftext ?? '',
thumbnail: item.thumbnail ?? '',
subreddit: item.subreddit ?? '',
})
} else if (child.kind === 't1') {
comments.push({
id: item.id ?? '',
name: item.name ?? '',
author: item.author || '[deleted]',
body: item.body ?? '',
score: item.score ?? 0,
created_utc: item.created_utc ?? 0,
permalink: item.permalink ? `https://www.reddit.com${item.permalink}` : '',
replies: [],
})
}
}
return {
success: true,
output: {
posts,
comments,
after: data.data?.after ?? null,
before: data.data?.before ?? null,
},
}
},
outputs: {
posts: {
type: 'array',
description: 'Array of saved posts (t3) with title, author, URL, score, and metadata',
items: {
type: 'object',
properties: POST_LISTING_OUTPUT_PROPERTIES,
},
},
comments: {
type: 'array',
description: 'Array of saved comments (t1) with author, body, score, and permalink',
items: {
type: 'object',
properties: COMMENT_LISTING_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,
},
},
}
+126
View File
@@ -0,0 +1,126 @@
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()
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 {
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.active_user_count ?? 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 },
},
}
@@ -0,0 +1,148 @@
import { normalizeSubreddit } from '@/tools/reddit/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface RedditSubredditRule {
short_name: string
description: string
description_html: string
violation_reason: string
kind: string
created_utc: number
priority: number
}
interface RedditGetSubredditRulesParams {
subreddit: string
accessToken?: string
}
interface RedditGetSubredditRulesResponse extends ToolResponse {
output: {
rules: RedditSubredditRule[]
site_rules: string[]
site_rules_flow: unknown[]
}
}
export const getSubredditRulesTool: ToolConfig<
RedditGetSubredditRulesParams,
RedditGetSubredditRulesResponse
> = {
id: 'reddit_get_subreddit_rules',
name: 'Get Subreddit Rules',
description: 'Get the rules and site-wide rules that apply to 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 rules for (e.g., "technology", "programming", "news")',
},
},
request: {
url: (params: RedditGetSubredditRulesParams) => {
const subreddit = normalizeSubreddit(params.subreddit)
return `https://oauth.reddit.com/r/${subreddit}/about/rules?raw_json=1`
},
method: 'GET',
headers: (params: RedditGetSubredditRulesParams) => {
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()
if (!response.ok) {
return {
success: false,
output: {
rules: [],
site_rules: [],
site_rules_flow: [],
},
}
}
const rules: RedditSubredditRule[] =
data.rules?.map((rule: any) => ({
short_name: rule.short_name ?? '',
description: rule.description ?? '',
description_html: rule.description_html ?? '',
violation_reason: rule.violation_reason ?? '',
kind: rule.kind ?? '',
created_utc: rule.created_utc ?? 0,
priority: rule.priority ?? 0,
})) || []
return {
success: true,
output: {
rules,
site_rules: data.site_rules ?? [],
site_rules_flow: data.site_rules_flow ?? [],
},
}
},
outputs: {
rules: {
type: 'array',
description: 'Array of subreddit-specific rules',
items: {
type: 'object',
properties: {
short_name: { type: 'string', description: 'Short name/title of the rule' },
description: { type: 'string', description: 'Full description of the rule (markdown)' },
description_html: {
type: 'string',
description: 'HTML-rendered rule description',
optional: true,
},
violation_reason: {
type: 'string',
description: 'Reason shown on the report menu when this rule is selected',
},
kind: {
type: 'string',
description: 'What the rule applies to: "link", "comment", or "all"',
},
created_utc: { type: 'number', description: 'Creation time in UTC epoch seconds' },
priority: { type: 'number', description: 'Display/order priority of the rule' },
},
},
},
site_rules: {
type: 'array',
description: 'Reddit site-wide rules that apply to the subreddit',
items: { type: 'string', description: 'Site-wide rule text' },
},
site_rules_flow: {
type: 'array',
description: 'Structured site-wide rules flow used by the report menu',
items: { type: 'object', description: 'Site-wide rule flow node' },
},
},
}
+106
View File
@@ -0,0 +1,106 @@
import { validatePathSegment } from '@/lib/core/security/input-validation'
import type { RedditGetUserParams, RedditUserResponse } from '@/tools/reddit/types'
import type { ToolConfig } from '@/tools/types'
export const getUserTool: ToolConfig<RedditGetUserParams, RedditUserResponse> = {
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\//, '')
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',
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()
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 {
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' },
},
}
+221
View File
@@ -0,0 +1,221 @@
import { validateEnum, validatePathSegment } from '@/lib/core/security/input-validation'
import { COMMENT_LISTING_OUTPUT_PROPERTIES, type RedditComment } from '@/tools/reddit/types'
import type { ToolConfig, ToolResponse } from '@/tools/types'
const ALLOWED_SORT_OPTIONS = ['hot', 'new', 'top', 'controversial'] as const
interface GetUserCommentsParams {
username: string
sort?: 'hot' | 'new' | 'top' | 'controversial'
time?: 'hour' | 'day' | 'week' | 'month' | 'year' | 'all'
limit?: number
after?: string
before?: string
count?: number
show?: string
sr_detail?: boolean
accessToken: string
}
interface GetUserCommentsResponse extends ToolResponse {
output: {
comments: RedditComment[]
after: string | null
before: string | null
}
}
export const getUserCommentsTool: ToolConfig<GetUserCommentsParams, GetUserCommentsResponse> = {
id: 'reddit_get_user_comments',
name: 'Get Reddit User Comments',
description: 'Fetch comments (t1) made by 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',
},
username: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Reddit username whose comments to fetch (e.g., "spez", "example_user")',
},
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Sort method for comments: "hot", "new", "top", "controversial" (default: "new")',
},
time: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Time filter for "top"/"controversial" sorts: "hour", "day", "week", "month", "year", or "all" (default: "all")',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of comments 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)',
},
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")',
},
sr_detail: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Expand subreddit details in the response',
},
},
request: {
url: (params: GetUserCommentsParams) => {
const username = params.username.trim().replace(/^u\//, '')
const validation = validatePathSegment(username, { paramName: 'username' })
if (!validation.isValid) {
throw new Error(validation.error)
}
const limit = Math.min(Math.max(1, params.limit ?? 25), 100)
const urlParams = new URLSearchParams({
limit: limit.toString(),
raw_json: '1',
})
if (params.sort !== undefined && params.sort !== null) {
const sortValidation = validateEnum(params.sort, ALLOWED_SORT_OPTIONS, 'sort')
if (!sortValidation.isValid) {
throw new Error(sortValidation.error)
}
urlParams.append('sort', params.sort)
if (
(params.sort === 'top' || params.sort === 'controversial') &&
params.time !== undefined &&
params.time !== null
) {
urlParams.append('t', params.time)
}
}
if (params.after !== undefined && params.after !== null && params.after !== '')
urlParams.append('after', params.after)
if (params.before !== undefined && params.before !== null && params.before !== '')
urlParams.append('before', params.before)
if (params.count !== undefined && params.count !== null)
urlParams.append('count', params.count.toString())
if (params.show !== undefined && params.show !== null && params.show !== '')
urlParams.append('show', params.show)
if (params.sr_detail !== undefined && params.sr_detail !== null)
urlParams.append('sr_detail', params.sr_detail.toString())
return `https://oauth.reddit.com/user/${username}/comments?${urlParams.toString()}`
},
method: 'GET',
headers: (params: GetUserCommentsParams) => {
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()
if (!response.ok) {
return {
success: false,
output: {
comments: [],
after: null,
before: null,
},
}
}
const comments: RedditComment[] =
data.data?.children?.map((child: any) => {
const comment = child.data || {}
return {
id: comment.id ?? '',
name: comment.name ?? '',
author: comment.author || '[deleted]',
body: comment.body ?? '',
score: comment.score ?? 0,
created_utc: comment.created_utc ?? 0,
permalink: comment.permalink ? `https://www.reddit.com${comment.permalink}` : '',
replies: [],
}
}) || []
return {
success: true,
output: {
comments,
after: data.data?.after ?? null,
before: data.data?.before ?? null,
},
}
},
outputs: {
comments: {
type: 'array',
description: 'Array of comments with author, body, score, timestamp, and permalink',
items: {
type: 'object',
properties: COMMENT_LISTING_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,
},
},
}
+225
View File
@@ -0,0 +1,225 @@
import { validateEnum, validatePathSegment } from '@/lib/core/security/input-validation'
import { POST_LISTING_OUTPUT_PROPERTIES, type RedditPost } from '@/tools/reddit/types'
import type { ToolConfig, ToolResponse } from '@/tools/types'
const ALLOWED_SORT_OPTIONS = ['hot', 'new', 'top', 'controversial'] as const
interface GetUserPostsParams {
username: string
sort?: 'hot' | 'new' | 'top' | 'controversial'
time?: 'hour' | 'day' | 'week' | 'month' | 'year' | 'all'
limit?: number
after?: string
before?: string
count?: number
show?: string
sr_detail?: boolean
accessToken: string
}
interface GetUserPostsResponse extends ToolResponse {
output: {
posts: RedditPost[]
after: string | null
before: string | null
}
}
export const getUserPostsTool: ToolConfig<GetUserPostsParams, GetUserPostsResponse> = {
id: 'reddit_get_user_posts',
name: 'Get Reddit User Posts',
description: 'Fetch submitted posts (t3) from a Reddit user profile',
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 whose posts to fetch (e.g., "spez", "example_user")',
},
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort method for posts: "hot", "new", "top", "controversial" (default: "new")',
},
time: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Time filter for "top"/"controversial" sorts: "hour", "day", "week", "month", "year", or "all" (default: "all")',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of posts 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)',
},
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")',
},
sr_detail: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Expand subreddit details in the response',
},
},
request: {
url: (params: GetUserPostsParams) => {
const username = params.username.trim().replace(/^u\//, '')
const validation = validatePathSegment(username, { paramName: 'username' })
if (!validation.isValid) {
throw new Error(validation.error)
}
const limit = Math.min(Math.max(1, params.limit ?? 25), 100)
const urlParams = new URLSearchParams({
limit: limit.toString(),
raw_json: '1',
})
if (params.sort !== undefined && params.sort !== null) {
const sortValidation = validateEnum(params.sort, ALLOWED_SORT_OPTIONS, 'sort')
if (!sortValidation.isValid) {
throw new Error(sortValidation.error)
}
urlParams.append('sort', params.sort)
if (
(params.sort === 'top' || params.sort === 'controversial') &&
params.time !== undefined &&
params.time !== null
) {
urlParams.append('t', params.time)
}
}
if (params.after !== undefined && params.after !== null && params.after !== '')
urlParams.append('after', params.after)
if (params.before !== undefined && params.before !== null && params.before !== '')
urlParams.append('before', params.before)
if (params.count !== undefined && params.count !== null)
urlParams.append('count', params.count.toString())
if (params.show !== undefined && params.show !== null && params.show !== '')
urlParams.append('show', params.show)
if (params.sr_detail !== undefined && params.sr_detail !== null)
urlParams.append('sr_detail', params.sr_detail.toString())
return `https://oauth.reddit.com/user/${username}/submitted?${urlParams.toString()}`
},
method: 'GET',
headers: (params: GetUserPostsParams) => {
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()
if (!response.ok) {
return {
success: false,
output: {
posts: [],
after: null,
before: null,
},
}
}
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,
is_self: !!post.is_self,
selftext: post.selftext ?? '',
thumbnail: post.thumbnail ?? '',
subreddit: post.subreddit ?? '',
}
}) || []
return {
success: true,
output: {
posts,
after: data.data?.after ?? null,
before: data.data?.before ?? null,
},
}
},
outputs: {
posts: {
type: 'array',
description: 'Array of submitted posts with title, author, URL, score, and metadata',
items: {
type: 'object',
properties: POST_LISTING_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,
},
},
}
+179
View File
@@ -0,0 +1,179 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface RedditHideParams {
id: string
accessToken?: string
}
interface RedditHideResponse extends ToolResponse {
output: {
success: boolean
message?: string
}
}
export const hideTool: ToolConfig<RedditHideParams, RedditHideResponse> = {
id: 'reddit_hide',
name: 'Hide Reddit Post',
description: 'Hide one or more Reddit posts from your listings',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated list of post fullnames to hide (e.g., "t3_abc123,t3_def456")',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/hide',
method: 'POST',
headers: (params: RedditHideParams) => {
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: RedditHideParams) => {
const formData = new URLSearchParams({
id: params.id,
})
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditHideParams) => {
await response.json().catch(() => ({}))
if (response.ok) {
return {
success: true,
output: {
success: true,
message: `Successfully hid ${requestParams?.id}`,
},
}
}
return {
success: false,
output: {
success: false,
message: 'Failed to hide post',
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the hide was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
export const unhideTool: ToolConfig<RedditHideParams, RedditHideResponse> = {
id: 'reddit_unhide',
name: 'Unhide Reddit Post',
description: 'Unhide one or more previously hidden Reddit posts',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated list of post fullnames to unhide (e.g., "t3_abc123,t3_def456")',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/unhide',
method: 'POST',
headers: (params: RedditHideParams) => {
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: RedditHideParams) => {
const formData = new URLSearchParams({
id: params.id,
})
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditHideParams) => {
await response.json().catch(() => ({}))
if (response.ok) {
return {
success: true,
output: {
success: true,
message: `Successfully unhid ${requestParams?.id}`,
},
}
}
return {
success: false,
output: {
success: false,
message: 'Failed to unhide post',
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the unhide was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
+142
View File
@@ -0,0 +1,142 @@
import type { RedditHotPostsResponse, RedditPost } from '@/tools/reddit/types'
import { normalizeSubreddit } from '@/tools/reddit/utils'
import type { ToolConfig } from '@/tools/types'
interface HotPostsParams {
subreddit: string
limit?: number
accessToken: string
}
export const hotPostsTool: ToolConfig<HotPostsParams, RedditHotPostsResponse> = {
id: 'reddit_hot_posts',
name: 'Reddit Hot Posts',
description: 'Fetch the most popular (hot) posts from a specified 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 fetch hot posts from (e.g., "technology", "news")',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of posts to return (e.g., 25). Default: 10, max: 100',
},
},
request: {
url: (params) => {
const subreddit = normalizeSubreddit(params.subreddit)
const limit = Math.min(Math.max(1, params.limit ?? 10), 100)
return `https://oauth.reddit.com/r/${subreddit}/hot?limit=${limit}&raw_json=1`
},
method: 'GET',
headers: (params: HotPostsParams) => {
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, requestParams?: HotPostsParams) => {
const data = await response.json()
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 ?? '',
}
}) || []
const subreddit =
data.data?.children?.[0]?.data?.subreddit ||
(posts.length > 0 ? posts[0].subreddit : requestParams?.subreddit || '')
return {
success: true,
output: {
subreddit,
posts,
after: data.data?.after ?? null,
before: data.data?.before ?? null,
},
}
},
outputs: {
subreddit: {
type: 'string',
description: 'Name of the subreddit where hot posts were fetched from',
},
posts: {
type: 'array',
description:
'Array of hot posts with title, author, URL, score, comments count, and metadata',
items: {
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' },
permalink: { type: 'string', description: 'Reddit permalink' },
score: { type: 'number', description: 'Post score (upvotes - downvotes)' },
num_comments: { type: 'number', description: 'Number of comments' },
created_utc: { type: 'number', description: 'Creation timestamp (UTC)' },
is_self: { type: 'boolean', description: 'Whether this is a text post' },
selftext: { type: 'string', description: 'Text content for self posts' },
thumbnail: { type: 'string', description: 'Thumbnail URL' },
subreddit: { type: 'string', description: 'Subreddit name' },
},
},
},
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,
},
},
}
+72
View File
@@ -0,0 +1,72 @@
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 { getInfoTool } from '@/tools/reddit/get_info'
import { getMeTool } from '@/tools/reddit/get_me'
import { getMessagesTool } from '@/tools/reddit/get_messages'
import { getPostsTool } from '@/tools/reddit/get_posts'
import { getSavedTool } from '@/tools/reddit/get_saved'
import { getSubredditInfoTool } from '@/tools/reddit/get_subreddit_info'
import { getSubredditRulesTool } from '@/tools/reddit/get_subreddit_rules'
import { getUserTool } from '@/tools/reddit/get_user'
import { getUserCommentsTool } from '@/tools/reddit/get_user_comments'
import { getUserPostsTool } from '@/tools/reddit/get_user_posts'
import { hideTool, unhideTool } from '@/tools/reddit/hide'
import { hotPostsTool } from '@/tools/reddit/hot_posts'
import { listMySubredditsTool } from '@/tools/reddit/list_my_subreddits'
import { markAllReadTool, markReadTool } from '@/tools/reddit/mark_message'
import { markNsfwTool, unmarkNsfwTool } from '@/tools/reddit/mark_nsfw'
import { modApproveTool } from '@/tools/reddit/mod_approve'
import { modDistinguishTool } from '@/tools/reddit/mod_distinguish'
import { lockTool, unlockTool } from '@/tools/reddit/mod_lock'
import { modRemoveTool } from '@/tools/reddit/mod_remove'
import { modStickyTool } from '@/tools/reddit/mod_sticky'
import { replyTool } from '@/tools/reddit/reply'
import { reportTool } from '@/tools/reddit/report'
import { saveTool, unsaveTool } from '@/tools/reddit/save'
import { searchTool } from '@/tools/reddit/search'
import { searchSubredditsTool } from '@/tools/reddit/search_subreddits'
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'
export const redditHotPostsTool = hotPostsTool
export const redditGetPostsTool = getPostsTool
export const redditGetCommentsTool = getCommentsTool
export const redditGetControversialTool = getControversialTool
export const redditSearchTool = searchTool
export const redditSubmitPostTool = submitPostTool
export const redditVoteTool = voteTool
export const redditSaveTool = saveTool
export const redditUnsaveTool = unsaveTool
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
export const redditGetSubredditRulesTool = getSubredditRulesTool
export const redditGetUserPostsTool = getUserPostsTool
export const redditGetUserCommentsTool = getUserCommentsTool
export const redditGetSavedTool = getSavedTool
export const redditGetInfoTool = getInfoTool
export const redditSearchSubredditsTool = searchSubredditsTool
export const redditListMySubredditsTool = listMySubredditsTool
export const redditReportTool = reportTool
export const redditHideTool = hideTool
export const redditUnhideTool = unhideTool
export const redditMarkNsfwTool = markNsfwTool
export const redditUnmarkNsfwTool = unmarkNsfwTool
export const redditMarkReadTool = markReadTool
export const redditMarkAllReadTool = markAllReadTool
export const redditModApproveTool = modApproveTool
export const redditModRemoveTool = modRemoveTool
export const redditModDistinguishTool = modDistinguishTool
export const redditLockTool = lockTool
export const redditUnlockTool = unlockTool
export const redditModStickyTool = modStickyTool
+213
View File
@@ -0,0 +1,213 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface RedditSubredditSummary {
id: string
name: string
display_name: string
title: string
public_description: string
subscribers: number
over18: boolean
url: string
subreddit_type: string
icon_img: string | null
created_utc: number
accounts_active: number
}
interface RedditListMySubredditsParams {
limit?: number
after?: string
before?: string
count?: number
show?: string
sr_detail?: boolean
accessToken?: string
}
interface RedditListMySubredditsResponse extends ToolResponse {
output: {
subreddits: RedditSubredditSummary[]
after: string | null
before: string | null
}
}
export const listMySubredditsTool: ToolConfig<
RedditListMySubredditsParams,
RedditListMySubredditsResponse
> = {
id: 'reddit_list_my_subreddits',
name: 'List My Subreddits',
description: 'List the subreddits the authenticated user is subscribed to',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of subreddits 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)',
},
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")',
},
sr_detail: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Expand subreddit details in the response',
},
},
request: {
url: (params: RedditListMySubredditsParams) => {
const limit = Math.min(Math.max(1, params.limit ?? 25), 100)
const urlParams = new URLSearchParams({
limit: limit.toString(),
raw_json: '1',
})
if (params.after !== undefined && params.after !== null && params.after !== '')
urlParams.append('after', params.after)
if (params.before !== undefined && params.before !== null && params.before !== '')
urlParams.append('before', params.before)
if (params.count !== undefined && params.count !== null)
urlParams.append('count', params.count.toString())
if (params.show !== undefined && params.show !== null && params.show !== '')
urlParams.append('show', params.show)
if (params.sr_detail !== undefined && params.sr_detail !== null)
urlParams.append('sr_detail', params.sr_detail.toString())
return `https://oauth.reddit.com/subreddits/mine/subscriber?${urlParams.toString()}`
},
method: 'GET',
headers: (params: RedditListMySubredditsParams) => {
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()
if (!response.ok) {
return {
success: false,
output: {
subreddits: [],
after: null,
before: null,
},
}
}
const subreddits: RedditSubredditSummary[] =
data.data?.children?.map((child: any) => {
const sub = child.data || {}
return {
id: sub.id ?? '',
name: sub.name ?? '',
display_name: sub.display_name ?? '',
title: sub.title ?? '',
public_description: sub.public_description ?? '',
subscribers: sub.subscribers ?? 0,
over18: sub.over18 ?? false,
url: sub.url ?? '',
subreddit_type: sub.subreddit_type ?? '',
icon_img: sub.icon_img ?? null,
created_utc: sub.created_utc ?? 0,
accounts_active: sub.active_user_count ?? sub.accounts_active ?? 0,
}
}) || []
return {
success: true,
output: {
subreddits,
after: data.data?.after ?? null,
before: data.data?.before ?? null,
},
}
},
outputs: {
subreddits: {
type: 'array',
description: 'Array of subscribed subreddits with name, description, and subscriber metadata',
items: {
type: 'object',
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' },
public_description: { type: 'string', description: 'Short public description' },
subscribers: { type: 'number', description: 'Number of subscribers' },
over18: { type: 'boolean', description: 'Whether the subreddit is NSFW' },
url: { type: 'string', description: 'Subreddit URL path (e.g., /r/technology/)' },
subreddit_type: {
type: 'string',
description: 'Subreddit type: public, private, restricted, etc.',
},
icon_img: { type: 'string', description: 'Subreddit icon URL', optional: true },
created_utc: { type: 'number', description: 'Creation time in UTC epoch seconds' },
accounts_active: {
type: 'number',
description: 'Number of currently active users',
},
},
},
},
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,
},
},
}
+170
View File
@@ -0,0 +1,170 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface RedditMarkReadParams {
id: string
accessToken?: string
}
interface RedditMarkAllReadParams {
accessToken?: string
}
interface RedditMarkMessageResponse extends ToolResponse {
output: {
success: boolean
message?: string
}
}
export const markReadTool: ToolConfig<RedditMarkReadParams, RedditMarkMessageResponse> = {
id: 'reddit_mark_read',
name: 'Mark Reddit Messages Read',
description: 'Mark one or more private messages as read',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Comma-separated list of message fullnames to mark read (e.g., "t4_abc123,t4_def456")',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/read_message',
method: 'POST',
headers: (params: RedditMarkReadParams) => {
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: RedditMarkReadParams) => {
const formData = new URLSearchParams({
id: params.id,
})
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditMarkReadParams) => {
await response.json().catch(() => ({}))
if (response.ok) {
return {
success: true,
output: {
success: true,
message: `Successfully marked ${requestParams?.id} as read`,
},
}
}
return {
success: false,
output: {
success: false,
message: 'Failed to mark messages as read',
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the operation was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
export const markAllReadTool: ToolConfig<RedditMarkAllReadParams, RedditMarkMessageResponse> = {
id: 'reddit_mark_all_read',
name: 'Mark All Reddit Messages Read',
description: 'Mark all private messages in the inbox as read',
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/read_all_messages',
method: 'POST',
headers: (params: RedditMarkAllReadParams) => {
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: () => '',
},
transformResponse: async (response: Response) => {
if (response.ok) {
return {
success: true,
output: {
success: true,
message: 'Successfully marked all messages as read',
},
}
}
return {
success: false,
output: {
success: false,
message: 'Failed to mark all messages as read',
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the operation was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
+179
View File
@@ -0,0 +1,179 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface RedditMarkNsfwParams {
id: string
accessToken?: string
}
interface RedditMarkNsfwResponse extends ToolResponse {
output: {
success: boolean
message?: string
}
}
export const markNsfwTool: ToolConfig<RedditMarkNsfwParams, RedditMarkNsfwResponse> = {
id: 'reddit_marknsfw',
name: 'Mark Reddit Post NSFW',
description: 'Mark a Reddit post as NSFW (not safe for work)',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Post fullname to mark as NSFW (e.g., "t3_abc123")',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/marknsfw',
method: 'POST',
headers: (params: RedditMarkNsfwParams) => {
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: RedditMarkNsfwParams) => {
const formData = new URLSearchParams({
id: params.id,
})
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditMarkNsfwParams) => {
await response.json().catch(() => ({}))
if (response.ok) {
return {
success: true,
output: {
success: true,
message: `Successfully marked ${requestParams?.id} as NSFW`,
},
}
}
return {
success: false,
output: {
success: false,
message: 'Failed to mark post as NSFW',
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the operation was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
export const unmarkNsfwTool: ToolConfig<RedditMarkNsfwParams, RedditMarkNsfwResponse> = {
id: 'reddit_unmarknsfw',
name: 'Unmark Reddit Post NSFW',
description: 'Remove the NSFW (not safe for work) mark from a Reddit post',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Post fullname to unmark as NSFW (e.g., "t3_abc123")',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/unmarknsfw',
method: 'POST',
headers: (params: RedditMarkNsfwParams) => {
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: RedditMarkNsfwParams) => {
const formData = new URLSearchParams({
id: params.id,
})
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditMarkNsfwParams) => {
await response.json().catch(() => ({}))
if (response.ok) {
return {
success: true,
output: {
success: true,
message: `Successfully unmarked ${requestParams?.id} as NSFW`,
},
}
}
return {
success: false,
output: {
success: false,
message: 'Failed to unmark post as NSFW',
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the operation was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
+97
View File
@@ -0,0 +1,97 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface RedditModApproveParams {
accessToken: string
id: string
}
interface RedditModApproveResponse extends ToolResponse {
output: {
success: boolean
message?: string
}
}
export const modApproveTool: ToolConfig<RedditModApproveParams, RedditModApproveResponse> = {
id: 'reddit_mod_approve',
name: 'Approve Reddit Post/Comment (Mod)',
description: 'Approve a reported or removed Reddit post or comment as a moderator',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Thing fullname to approve (e.g., "t3_abc123" for post, "t1_def456" for comment)',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/approve',
method: 'POST',
headers: (params: RedditModApproveParams) => {
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: RedditModApproveParams) => {
const formData = new URLSearchParams({
id: params.id,
})
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditModApproveParams) => {
await response.json().catch(() => ({}))
if (response.ok) {
return {
success: true,
output: {
success: true,
message: `Successfully approved ${requestParams?.id}`,
},
}
}
return {
success: false,
output: {
success: false,
message: 'Failed to approve item',
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the approval was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
+140
View File
@@ -0,0 +1,140 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
type RedditDistinguishHow = 'yes' | 'no' | 'admin' | 'special'
interface RedditModDistinguishParams {
accessToken: string
id: string
how: RedditDistinguishHow
sticky?: boolean
}
interface RedditModDistinguishResponse extends ToolResponse {
output: {
success: boolean
message?: string
}
}
const ALLOWED_HOW: RedditDistinguishHow[] = ['yes', 'no', 'admin', 'special']
export const modDistinguishTool: ToolConfig<
RedditModDistinguishParams,
RedditModDistinguishResponse
> = {
id: 'reddit_mod_distinguish',
name: 'Distinguish Reddit Post/Comment (Mod)',
description: 'Distinguish or un-distinguish a Reddit post or comment as a moderator',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Thing fullname to distinguish (e.g., "t3_abc123" for post, "t1_def456" for comment)',
},
how: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Distinguish type: "yes" (moderator), "no" (remove distinction), "admin", or "special"',
},
sticky: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Sticky the comment to the top of the comment page (comments only)',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/distinguish',
method: 'POST',
headers: (params: RedditModDistinguishParams) => {
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: RedditModDistinguishParams) => {
if (!ALLOWED_HOW.includes(params.how)) {
throw new Error('how must be one of "yes", "no", "admin", or "special"')
}
const formData = new URLSearchParams({
id: params.id,
how: params.how,
api_type: 'json',
})
if (params.sticky !== undefined) {
formData.append('sticky', params.sticky.toString())
}
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditModDistinguishParams) => {
const data = await response.json().catch(() => ({}) as any)
if (!response.ok) {
return {
success: false,
output: {
success: false,
message: `HTTP error ${response.status}`,
},
}
}
if (data.json?.errors && data.json.errors.length > 0) {
const errors = data.json.errors.map((err: string[]) => err.join(': ')).join(', ')
return {
success: false,
output: {
success: false,
message: `Failed to distinguish item: ${errors}`,
},
}
}
return {
success: true,
output: {
success: true,
message: `Successfully distinguished ${requestParams?.id}`,
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the distinguish action was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
+179
View File
@@ -0,0 +1,179 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface RedditLockParams {
accessToken: string
id: string
}
interface RedditLockResponse extends ToolResponse {
output: {
success: boolean
message?: string
}
}
export const lockTool: ToolConfig<RedditLockParams, RedditLockResponse> = {
id: 'reddit_lock',
name: 'Lock Reddit Post/Comment (Mod)',
description: 'Lock a Reddit post or comment to prevent further replies (moderator action)',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Thing fullname to lock (e.g., "t3_abc123" for post, "t1_def456" for comment)',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/lock',
method: 'POST',
headers: (params: RedditLockParams) => {
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: RedditLockParams) => {
const formData = new URLSearchParams({
id: params.id,
})
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditLockParams) => {
await response.json().catch(() => ({}))
if (response.ok) {
return {
success: true,
output: {
success: true,
message: `Successfully locked ${requestParams?.id}`,
},
}
}
return {
success: false,
output: {
success: false,
message: 'Failed to lock item',
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the lock was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
export const unlockTool: ToolConfig<RedditLockParams, RedditLockResponse> = {
id: 'reddit_unlock',
name: 'Unlock Reddit Post/Comment (Mod)',
description: 'Unlock a Reddit post or comment to allow replies again (moderator action)',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Thing fullname to unlock (e.g., "t3_abc123" for post, "t1_def456" for comment)',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/unlock',
method: 'POST',
headers: (params: RedditLockParams) => {
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: RedditLockParams) => {
const formData = new URLSearchParams({
id: params.id,
})
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditLockParams) => {
await response.json().catch(() => ({}))
if (response.ok) {
return {
success: true,
output: {
success: true,
message: `Successfully unlocked ${requestParams?.id}`,
},
}
}
return {
success: false,
output: {
success: false,
message: 'Failed to unlock item',
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the unlock was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
+105
View File
@@ -0,0 +1,105 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface RedditModRemoveParams {
accessToken: string
id: string
spam?: boolean
}
interface RedditModRemoveResponse extends ToolResponse {
output: {
success: boolean
message?: string
}
}
export const modRemoveTool: ToolConfig<RedditModRemoveParams, RedditModRemoveResponse> = {
id: 'reddit_mod_remove',
name: 'Remove Reddit Post/Comment (Mod)',
description: 'Remove a Reddit post or comment as a moderator, optionally marking it as spam',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Thing fullname to remove (e.g., "t3_abc123" for post, "t1_def456" for comment)',
},
spam: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Mark the item as spam to train the subreddit spam filter (default: false)',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/remove',
method: 'POST',
headers: (params: RedditModRemoveParams) => {
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: RedditModRemoveParams) => {
const formData = new URLSearchParams({
id: params.id,
spam: (params.spam ?? false).toString(),
})
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditModRemoveParams) => {
await response.json().catch(() => ({}))
if (response.ok) {
const asSpam = requestParams?.spam ? ' as spam' : ''
return {
success: true,
output: {
success: true,
message: `Successfully removed ${requestParams?.id}${asSpam}`,
},
}
}
return {
success: false,
output: {
success: false,
message: 'Failed to remove item',
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the removal was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
+135
View File
@@ -0,0 +1,135 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface RedditModStickyParams {
accessToken: string
id: string
state: boolean
num?: number
}
interface RedditModStickyResponse extends ToolResponse {
output: {
success: boolean
message?: string
}
}
export const modStickyTool: ToolConfig<RedditModStickyParams, RedditModStickyResponse> = {
id: 'reddit_mod_sticky',
name: 'Sticky Reddit Post (Mod)',
description: 'Sticky or unsticky a Reddit post to the top of a subreddit (moderator action)',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Post fullname to sticky/unsticky (e.g., "t3_abc123")',
},
state: {
type: 'boolean',
required: true,
visibility: 'user-or-llm',
description: 'true to sticky the post, false to unsticky it',
},
num: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Sticky slot to use, 1-4 (1 is the top slot). Only applies when stickying',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/set_subreddit_sticky',
method: 'POST',
headers: (params: RedditModStickyParams) => {
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: RedditModStickyParams) => {
if (typeof params.state !== 'boolean') {
throw new Error('state must be a boolean (true to sticky, false to unsticky)')
}
const formData = new URLSearchParams({
id: params.id,
state: params.state.toString(),
api_type: 'json',
})
if (params.num !== undefined) {
if (params.num < 1 || params.num > 4) {
throw new Error('num must be between 1 and 4')
}
formData.append('num', params.num.toString())
}
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditModStickyParams) => {
const data = await response.json().catch(() => ({}) as any)
if (!response.ok) {
return {
success: false,
output: {
success: false,
message: `HTTP error ${response.status}`,
},
}
}
if (data.json?.errors && data.json.errors.length > 0) {
const errors = data.json.errors.map((err: string[]) => err.join(': ')).join(', ')
return {
success: false,
output: {
success: false,
message: `Failed to set sticky: ${errors}`,
},
}
}
const action = requestParams?.state ? 'stickied' : 'unstickied'
return {
success: true,
output: {
success: true,
message: `Successfully ${action} ${requestParams?.id}`,
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the sticky action was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
+134
View File
@@ -0,0 +1,134 @@
import type { RedditReplyParams, RedditWriteResponse } from '@/tools/reddit/types'
import type { ToolConfig } from '@/tools/types'
export const replyTool: ToolConfig<RedditReplyParams, RedditWriteResponse> = {
id: 'reddit_reply',
name: 'Reply to Reddit Post/Comment',
description: 'Add a comment reply to a Reddit post or comment',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
parent_id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Thing fullname to reply to (e.g., "t3_abc123" for post, "t1_def456" for comment)',
},
text: {
type: 'string',
required: true,
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: {
url: () => 'https://oauth.reddit.com/api/comment',
method: 'POST',
headers: (params: RedditReplyParams) => {
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: RedditReplyParams) => {
const formData = new URLSearchParams({
thing_id: params.parent_id,
text: params.text,
api_type: 'json',
})
if (params.return_rtjson !== undefined)
formData.append('return_rtjson', params.return_rtjson.toString())
return formData.toString()
},
},
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}`,
},
}
}
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 post reply: ${errors}`,
},
}
}
const commentData = data.json?.data?.things?.[0]?.data
return {
success: true,
output: {
success: true,
message: 'Reply posted successfully',
data: {
id: commentData?.id,
name: commentData?.name,
permalink: commentData?.permalink
? `https://www.reddit.com${commentData.permalink}`
: undefined,
body: commentData?.body,
},
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the reply was posted successfully',
},
message: {
type: 'string',
description: 'Success or error message',
},
data: {
type: 'object',
description: 'Comment data including ID, name, permalink, and body',
properties: {
id: { type: 'string', description: 'New comment ID' },
name: { type: 'string', description: 'Thing fullname (t1_xxxxx)' },
permalink: { type: 'string', description: 'Comment permalink', optional: true },
body: { type: 'string', description: 'Comment body text', optional: true },
},
},
},
}
+130
View File
@@ -0,0 +1,130 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface RedditReportParams {
thing_id: string
reason?: string
other_reason?: string
accessToken?: string
}
interface RedditReportResponse extends ToolResponse {
output: {
success: boolean
message?: string
}
}
export const reportTool: ToolConfig<RedditReportParams, RedditReportResponse> = {
id: 'reddit_report',
name: 'Report Reddit Post/Comment',
description: 'Report a Reddit post or comment to subreddit moderators for a rules violation',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
thing_id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Thing fullname to report (e.g., "t3_abc123" for post, "t1_def456" for comment)',
},
reason: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Reason for reporting (max 100 characters)',
},
other_reason: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Free-form custom reason for reporting (max 100 characters)',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/report',
method: 'POST',
headers: (params: RedditReportParams) => {
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: RedditReportParams) => {
const formData = new URLSearchParams({
thing_id: params.thing_id,
api_type: 'json',
})
if (params.reason) {
formData.append('reason', params.reason)
}
if (params.other_reason) {
formData.append('other_reason', params.other_reason)
}
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditReportParams) => {
const data = await response.json().catch(() => ({}) as any)
if (!response.ok) {
return {
success: false,
output: {
success: false,
message: `HTTP error ${response.status}`,
},
}
}
if (data.json?.errors && data.json.errors.length > 0) {
const errors = data.json.errors.map((err: string[]) => err.join(': ')).join(', ')
return {
success: false,
output: {
success: false,
message: `Failed to report: ${errors}`,
},
}
}
return {
success: true,
output: {
success: true,
message: `Successfully reported ${requestParams?.thing_id}`,
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the report was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
+178
View File
@@ -0,0 +1,178 @@
import type { RedditSaveParams, RedditWriteResponse } from '@/tools/reddit/types'
import type { ToolConfig } from '@/tools/types'
export const saveTool: ToolConfig<RedditSaveParams, RedditWriteResponse> = {
id: 'reddit_save',
name: 'Save Reddit Post/Comment',
description: 'Save a Reddit post or comment to your saved items',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Thing fullname to save (e.g., "t3_abc123" for post, "t1_def456" for comment)',
},
category: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Category to save under (Reddit Gold feature)',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/save',
method: 'POST',
headers: (params: RedditSaveParams) => {
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: RedditSaveParams) => {
const formData = new URLSearchParams({
id: params.id,
})
if (params.category) {
formData.append('category', params.category)
}
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditSaveParams) => {
await response.json().catch(() => ({}))
if (response.ok) {
return {
success: true,
output: {
success: true,
message: `Successfully saved ${requestParams?.id}`,
},
}
}
return {
success: false,
output: {
success: false,
message: 'Failed to save item',
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the save was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
export const unsaveTool: ToolConfig<RedditSaveParams, RedditWriteResponse> = {
id: 'reddit_unsave',
name: 'Unsave Reddit Post/Comment',
description: 'Remove a Reddit post or comment from your saved items',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Thing fullname to unsave (e.g., "t3_abc123" for post, "t1_def456" for comment)',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/unsave',
method: 'POST',
headers: (params: RedditSaveParams) => {
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: RedditSaveParams) => {
const formData = new URLSearchParams({
id: params.id,
})
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditSaveParams) => {
await response.json().catch(() => ({}))
if (response.ok) {
return {
success: true,
output: {
success: true,
message: `Successfully unsaved ${requestParams?.id}`,
},
}
}
return {
success: false,
output: {
success: false,
message: 'Failed to unsave item',
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the unsave was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
+219
View File
@@ -0,0 +1,219 @@
import type { RedditPostsResponse, RedditSearchParams } from '@/tools/reddit/types'
import { normalizeSubreddit } from '@/tools/reddit/utils'
import type { ToolConfig } from '@/tools/types'
export const searchTool: ToolConfig<RedditSearchParams, RedditPostsResponse> = {
id: 'reddit_search',
name: 'Search Reddit',
description: 'Search for posts within 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 search in (e.g., "technology", "programming")',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Search query text (e.g., "artificial intelligence", "machine learning tutorial")',
},
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Sort method for search results (e.g., "relevance", "hot", "top", "new", "comments"). Default: "relevance"',
},
time: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Time filter for search results: "hour", "day", "week", "month", "year", or "all" (default: "all")',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of posts to return (e.g., 25). Default: 10, max: 100',
},
restrict_sr: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Restrict search to the specified subreddit only (default: true)',
},
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)',
},
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")',
},
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 restrict_sr = params.restrict_sr !== false
const urlParams = new URLSearchParams({
q: params.query,
sort: sort,
limit: limit.toString(),
restrict_sr: restrict_sr.toString(),
raw_json: '1',
})
if (params.time) {
urlParams.append('t', params.time)
}
if (params.after) urlParams.append('after', params.after)
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()}`
},
method: 'GET',
headers: (params: RedditSearchParams) => {
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, requestParams?: RedditSearchParams) => {
const data = await response.json()
const subredditName =
data.data?.children?.[0]?.data?.subreddit || requestParams?.subreddit || 'unknown'
const posts =
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,
is_self: !!post.is_self,
selftext: post.selftext ?? '',
thumbnail: post.thumbnail ?? '',
subreddit: post.subreddit ?? subredditName,
}
}) || []
return {
success: true,
output: {
subreddit: subredditName,
posts,
after: data.data?.after ?? null,
before: data.data?.before ?? null,
},
}
},
outputs: {
subreddit: {
type: 'string',
description: 'Name of the subreddit where search was performed',
},
posts: {
type: 'array',
description:
'Array of search result posts with title, author, URL, score, comments count, and metadata',
items: {
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' },
permalink: { type: 'string', description: 'Reddit permalink' },
score: { type: 'number', description: 'Post score (upvotes - downvotes)' },
num_comments: { type: 'number', description: 'Number of comments' },
created_utc: { type: 'number', description: 'Creation timestamp (UTC)' },
is_self: { type: 'boolean', description: 'Whether this is a text post' },
selftext: { type: 'string', description: 'Text content for self posts' },
thumbnail: { type: 'string', description: 'Thumbnail URL' },
subreddit: { type: 'string', description: 'Subreddit name' },
},
},
},
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,
},
},
}
+228
View File
@@ -0,0 +1,228 @@
import { validateEnum } from '@/lib/core/security/input-validation'
import type { ToolConfig, ToolResponse } from '@/tools/types'
const ALLOWED_SORT_OPTIONS = ['relevance', 'activity'] as const
interface RedditSubredditSummary {
id: string
name: string
display_name: string
title: string
public_description: string
subscribers: number
over18: boolean
url: string
subreddit_type: string
icon_img: string | null
created_utc: number
accounts_active: number
}
interface RedditSearchSubredditsParams {
q: string
limit?: number
after?: string
before?: string
sort?: 'relevance' | 'activity'
show_users?: boolean
sr_detail?: boolean
accessToken?: string
}
interface RedditSearchSubredditsResponse extends ToolResponse {
output: {
subreddits: RedditSubredditSummary[]
after: string | null
before: string | null
}
}
export const searchSubredditsTool: ToolConfig<
RedditSearchSubredditsParams,
RedditSearchSubredditsResponse
> = {
id: 'reddit_search_subreddits',
name: 'Search Subreddits',
description: 'Search for subreddits by name and description',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
q: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Search query to match against subreddit names and descriptions',
},
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort order for results: "relevance" or "activity". Default: "relevance"',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of subreddits 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)',
},
show_users: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to include matching user profiles in the results',
},
sr_detail: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Expand subreddit details in the response',
},
},
request: {
url: (params: RedditSearchSubredditsParams) => {
const sort = params.sort || 'relevance'
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 ?? 25), 100)
const urlParams = new URLSearchParams({
q: params.q ?? '',
sort,
limit: limit.toString(),
raw_json: '1',
})
if (params.after !== undefined && params.after !== null && params.after !== '')
urlParams.append('after', params.after)
if (params.before !== undefined && params.before !== null && params.before !== '')
urlParams.append('before', params.before)
if (params.show_users !== undefined && params.show_users !== null)
urlParams.append('show_users', params.show_users.toString())
if (params.sr_detail !== undefined && params.sr_detail !== null)
urlParams.append('sr_detail', params.sr_detail.toString())
return `https://oauth.reddit.com/subreddits/search?${urlParams.toString()}`
},
method: 'GET',
headers: (params: RedditSearchSubredditsParams) => {
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()
if (!response.ok) {
return {
success: false,
output: {
subreddits: [],
after: null,
before: null,
},
}
}
const subreddits: RedditSubredditSummary[] =
data.data?.children?.map((child: any) => {
const sub = child.data || {}
return {
id: sub.id ?? '',
name: sub.name ?? '',
display_name: sub.display_name ?? '',
title: sub.title ?? '',
public_description: sub.public_description ?? '',
subscribers: sub.subscribers ?? 0,
over18: sub.over18 ?? false,
url: sub.url ?? '',
subreddit_type: sub.subreddit_type ?? '',
icon_img: sub.icon_img ?? null,
created_utc: sub.created_utc ?? 0,
accounts_active: sub.active_user_count ?? sub.accounts_active ?? 0,
}
}) || []
return {
success: true,
output: {
subreddits,
after: data.data?.after ?? null,
before: data.data?.before ?? null,
},
}
},
outputs: {
subreddits: {
type: 'array',
description: 'Array of matching subreddits with name, description, and subscriber metadata',
items: {
type: 'object',
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' },
public_description: { type: 'string', description: 'Short public description' },
subscribers: { type: 'number', description: 'Number of subscribers' },
over18: { type: 'boolean', description: 'Whether the subreddit is NSFW' },
url: { type: 'string', description: 'Subreddit URL path (e.g., /r/technology/)' },
subreddit_type: {
type: 'string',
description: 'Subreddit type: public, private, restricted, etc.',
},
icon_img: { type: 'string', description: 'Subreddit icon URL', optional: true },
created_utc: { type: 'number', description: 'Creation time in UTC epoch seconds' },
accounts_active: {
type: 'number',
description: 'Number of currently active users',
},
},
},
},
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,
},
},
}
+122
View File
@@ -0,0 +1,122 @@
import type { RedditSendMessageParams, RedditWriteResponse } from '@/tools/reddit/types'
import type { ToolConfig } from '@/tools/types'
export const sendMessageTool: ToolConfig<RedditSendMessageParams, RedditWriteResponse> = {
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()
},
},
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 send message: ${errorMsg}`,
},
}
}
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',
},
},
}
+195
View File
@@ -0,0 +1,195 @@
import type { RedditSubmitParams, RedditWriteResponse } from '@/tools/reddit/types'
import { normalizeSubreddit } from '@/tools/reddit/utils'
import type { ToolConfig } from '@/tools/types'
export const submitPostTool: ToolConfig<RedditSubmitParams, RedditWriteResponse> = {
id: 'reddit_submit_post',
name: 'Submit Reddit Post',
description: 'Submit a new post to a subreddit (text or link)',
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 post to (e.g., "technology", "programming")',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Title of the submission (e.g., "Check out this new AI tool"). Max 300 characters',
},
text: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Text content for a self post in markdown format (e.g., "This is the **body** of my post")',
},
url: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'URL for a link post (cannot be used with text)',
},
nsfw: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Mark post as NSFW',
},
spoiler: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Mark post as spoiler',
},
send_replies: {
type: 'boolean',
required: false,
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: {
url: () => 'https://oauth.reddit.com/api/submit',
method: 'POST',
headers: (params: RedditSubmitParams) => {
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: RedditSubmitParams) => {
const subreddit = normalizeSubreddit(params.subreddit)
const formData = new URLSearchParams({
sr: subreddit,
title: params.title,
api_type: 'json',
})
if (params.text) {
formData.append('kind', 'self')
formData.append('text', params.text)
} else if (params.url) {
formData.append('kind', 'link')
formData.append('url', params.url)
} else {
formData.append('kind', 'self')
formData.append('text', '')
}
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())
return formData.toString()
},
},
transformResponse: async (response: Response) => {
const data = await response.json().catch(() => ({}) as any)
if (!response.ok) {
return {
success: false,
output: {
success: false,
message: `Failed to submit post: HTTP error ${response.status}`,
},
}
}
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 submit post: ${errors}`,
},
}
}
const postData = data.json?.data
return {
success: true,
output: {
success: true,
message: 'Post submitted successfully',
data: {
id: postData?.id,
name: postData?.name,
url: postData?.url,
permalink: postData?.permalink
? `https://www.reddit.com${postData.permalink}`
: (postData?.url ?? ''),
},
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the post was submitted successfully',
},
message: {
type: 'string',
description: 'Success or error message',
},
data: {
type: 'object',
description: 'Post data including ID, name, URL, and permalink',
properties: {
id: { type: 'string', description: 'New post ID' },
name: { type: 'string', description: 'Thing fullname (t3_xxxxx)', optional: true },
url: { type: 'string', description: 'Post URL from API response' },
permalink: { type: 'string', description: 'Full Reddit permalink', optional: true },
},
},
},
}
+105
View File
@@ -0,0 +1,105 @@
import type { RedditSubscribeParams, RedditWriteResponse } from '@/tools/reddit/types'
import { normalizeSubreddit } from '@/tools/reddit/utils'
import type { ToolConfig } from '@/tools/types'
export const subscribeTool: ToolConfig<RedditSubscribeParams, RedditWriteResponse> = {
id: 'reddit_subscribe',
name: 'Subscribe/Unsubscribe from Subreddit',
description: 'Subscribe or unsubscribe from 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 subscribe to or unsubscribe from (e.g., "technology", "programming")',
},
action: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Action to perform: "sub" to subscribe or "unsub" to unsubscribe',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/subscribe',
method: 'POST',
headers: (params: RedditSubscribeParams) => {
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: RedditSubscribeParams) => {
if (!['sub', 'unsub'].includes(params.action)) {
throw new Error('action must be "sub" or "unsub"')
}
const subreddit = normalizeSubreddit(params.subreddit)
const formData = new URLSearchParams({
action: params.action,
sr_name: subreddit,
})
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditSubscribeParams) => {
await response.json().catch(() => ({}))
if (response.ok) {
const actionText =
requestParams?.action === 'sub'
? `subscribed to r/${requestParams?.subreddit || 'subreddit'}`
: `unsubscribed from r/${requestParams?.subreddit || 'subreddit'}`
return {
success: true,
output: {
success: true,
message: `Successfully ${actionText}`,
},
}
}
return {
success: false,
output: {
success: false,
message: 'Failed to update subscription',
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the subscription action was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}
+518
View File
@@ -0,0 +1,518 @@
import type { OutputProperty, ToolResponse } from '@/tools/types'
/**
* Shared output property definitions for Reddit API responses.
* Based on official Reddit API documentation: https://github.com/reddit-archive/reddit/wiki/JSON
*/
/**
* Output definition for Reddit post (Link) objects.
* Implements votable and created interfaces per Reddit API docs.
*/
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: {
type: 'string',
description: 'Unformatted post content with markup (self posts only)',
},
thumbnail: { type: 'string', description: 'Image URL or "self"/"image"/"default"' },
subreddit: { type: 'string', description: 'Subreddit name without /r/ prefix' },
subreddit_id: { type: 'string', description: 'Subreddit identifier' },
domain: { type: 'string', description: 'Source domain; "self.<subreddit>" for self-posts' },
over_18: { type: 'boolean', description: 'NSFW tag status' },
locked: { type: 'boolean', description: 'Whether closed to new comments' },
stickied: { type: 'boolean', description: 'Sticky post designation' },
edited: { type: 'number', description: 'Edit timestamp or false if unedited' },
distinguished: {
type: 'string',
description: 'Moderator/admin status: null/"moderator"/"admin"/"special"',
},
ups: { type: 'number', description: 'Upvote count' },
downs: { type: 'number', description: 'Downvote count' },
likes: {
type: 'boolean',
description: 'User vote: true (upvote), false (downvote), null (none)',
},
saved: { type: 'boolean', description: 'Whether user saved the post' },
hidden: { type: 'boolean', description: 'Whether logged-in user hid the post' },
author_flair_text: { type: 'string', description: 'Text displayed as author flair' },
author_flair_css_class: { type: 'string', description: 'CSS styling for author flair' },
link_flair_text: { type: 'string', description: 'Text for post flair' },
link_flair_css_class: { type: 'string', description: 'CSS class for post flair' },
} as const satisfies Record<string, OutputProperty>
/**
* Output definition for Reddit comment objects.
* Implements votable and created interfaces per Reddit API docs.
*/
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' },
created_utc: { type: 'number', description: 'Creation time in UTC epoch seconds' },
score: { type: 'number', description: 'Net comment score' },
permalink: { type: 'string', description: 'Comment permalink URL' },
parent_id: { type: 'string', description: 'ID of parent (post or comment)' },
link_id: { type: 'string', description: 'Parent post identifier' },
subreddit: { type: 'string', description: 'Subreddit name without /r/ prefix' },
subreddit_id: { type: 'string', description: 'Subreddit identifier' },
edited: { type: 'number', description: 'UTC edit timestamp or false if unedited' },
distinguished: {
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)' },
saved: { type: 'boolean', description: 'User save status' },
score_hidden: { type: 'boolean', description: 'Score visibility status' },
gilded: { type: 'number', description: 'Reddit Gold awards received' },
author_flair_text: { type: 'string', description: 'Author flair text' },
author_flair_css_class: { type: 'string', description: 'CSS class for author flair' },
link_author: { type: 'string', description: 'Parent post author (when outside original thread)' },
link_title: { type: 'string', description: 'Parent post title (when outside original thread)' },
link_url: { type: 'string', description: 'Parent post URL (when outside original thread)' },
} as const satisfies Record<string, OutputProperty>
/**
* Simplified post output properties for listing responses (most commonly used fields)
*/
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' },
permalink: { type: 'string', description: 'Reddit permalink' },
score: { type: 'number', description: 'Post score (upvotes - downvotes)' },
num_comments: { type: 'number', description: 'Number of comments' },
created_utc: { type: 'number', description: 'Creation timestamp (UTC)' },
is_self: { type: 'boolean', description: 'Whether this is a text post' },
selftext: { type: 'string', description: 'Text content for self posts' },
thumbnail: { type: 'string', description: 'Thumbnail URL' },
subreddit: { type: 'string', description: 'Subreddit name' },
} as const satisfies Record<string, OutputProperty>
/**
* Simplified comment output properties for listing responses
*/
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' },
created_utc: { type: 'number', description: 'Creation timestamp' },
permalink: { type: 'string', description: 'Comment permalink' },
} as const satisfies Record<string, OutputProperty>
/**
* Comment with nested replies output definition
*/
export const COMMENT_WITH_REPLIES_OUTPUT_PROPERTIES = {
...COMMENT_LISTING_OUTPUT_PROPERTIES,
replies: {
type: 'array',
description: 'Nested reply comments',
items: { type: 'object', description: 'Nested comment with same structure' },
},
} as const satisfies Record<string, OutputProperty>
/**
* Post metadata output for get_comments tool
*/
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' },
score: { type: 'number', description: 'Post score' },
created_utc: { type: 'number', description: 'Creation timestamp' },
permalink: { type: 'string', description: 'Reddit permalink' },
} as const satisfies Record<string, OutputProperty>
/**
* Complete posts array output definition
*/
export const POSTS_ARRAY_OUTPUT: OutputProperty = {
type: 'array',
description: 'Array of posts with title, author, URL, score, comments count, and metadata',
items: {
type: 'object',
properties: POST_LISTING_OUTPUT_PROPERTIES,
},
}
/**
* Complete comments array output definition with nested replies
*/
export const COMMENTS_ARRAY_OUTPUT: OutputProperty = {
type: 'array',
description: 'Nested comments with author, body, score, timestamps, and replies',
items: {
type: 'object',
properties: COMMENT_WITH_REPLIES_OUTPUT_PROPERTIES,
},
}
/**
* Post metadata output definition for get_comments tool
*/
export const POST_METADATA_OUTPUT: OutputProperty = {
type: 'object',
description: 'Post information including ID, title, author, content, and metadata',
properties: POST_METADATA_OUTPUT_PROPERTIES,
}
/**
* Write operation success output properties
*/
export const WRITE_SUCCESS_OUTPUT_PROPERTIES = {
success: { type: 'boolean', description: 'Whether the operation was successful' },
message: { type: 'string', description: 'Success or error message' },
} as const satisfies Record<string, OutputProperty>
/**
* Submit post response data output properties
*/
export const SUBMIT_POST_DATA_OUTPUT_PROPERTIES = {
id: { type: 'string', description: 'New post ID' },
name: { type: 'string', description: 'Thing fullname (t3_xxxxx)' },
url: { type: 'string', description: 'Post URL from API response' },
permalink: { type: 'string', description: 'Full Reddit permalink' },
} as const satisfies Record<string, OutputProperty>
/**
* Submit post data output definition
*/
export const SUBMIT_POST_DATA_OUTPUT: OutputProperty = {
type: 'object',
description: 'Post data including ID, name, URL, and permalink',
properties: SUBMIT_POST_DATA_OUTPUT_PROPERTIES,
}
/**
* Reply comment response data output properties
*/
export const REPLY_DATA_OUTPUT_PROPERTIES = {
id: { type: 'string', description: 'New comment ID' },
name: { type: 'string', description: 'Thing fullname (t1_xxxxx)' },
permalink: { type: 'string', description: 'Comment permalink' },
body: { type: 'string', description: 'Comment body text' },
} as const satisfies Record<string, OutputProperty>
/**
* Reply data output definition
*/
export const REPLY_DATA_OUTPUT: OutputProperty = {
type: 'object',
description: 'Comment data including ID, name, permalink, and body',
properties: REPLY_DATA_OUTPUT_PROPERTIES,
}
/**
* Edit response data output properties
*/
export const EDIT_DATA_OUTPUT_PROPERTIES = {
id: { type: 'string', description: 'Edited thing ID' },
body: { type: 'string', description: 'Updated comment body (for comments)' },
selftext: { type: 'string', description: 'Updated post text (for self posts)' },
} as const satisfies Record<string, OutputProperty>
/**
* Edit data output definition
*/
export const EDIT_DATA_OUTPUT: OutputProperty = {
type: 'object',
description: 'Updated content data',
properties: EDIT_DATA_OUTPUT_PROPERTIES,
}
export interface RedditPost {
id: string
name: string
title: string
author: string
url: string
permalink: string
created_utc: number
score: number
num_comments: number
selftext?: string
thumbnail?: string
is_self: boolean
subreddit: string
}
export interface RedditComment {
id: string
name: string
author: string
body: string
created_utc: number
score: number
permalink: string
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
}
}
export interface RedditPostsParams {
subreddit: string
sort?: 'hot' | 'new' | 'top' | 'rising' | 'controversial'
limit?: number
time?: 'hour' | 'day' | 'week' | 'month' | 'year' | 'all'
after?: string
before?: string
count?: number
show?: string
sr_detail?: boolean
g?: string
accessToken?: string
}
export interface RedditPostsResponse extends ToolResponse {
output: {
subreddit: string
posts: RedditPost[]
after: string | null
before: string | null
}
}
export interface RedditCommentsParams {
postId: string
subreddit: string
sort?: 'confidence' | 'top' | 'new' | 'controversial' | 'old' | 'random' | 'qa'
limit?: number
depth?: number
context?: number
showedits?: boolean
showmore?: boolean
threaded?: boolean
truncate?: number
comment?: string
accessToken?: string
}
export interface RedditCommentsResponse extends ToolResponse {
output: {
post: {
id: string
name: string
title: string
author: string
selftext?: string
created_utc: number
score: number
permalink: string
}
comments: RedditComment[]
}
}
export interface RedditControversialParams {
subreddit: string
time?: 'hour' | 'day' | 'week' | 'month' | 'year' | 'all'
limit?: number
after?: string
before?: string
count?: number
show?: string
sr_detail?: boolean
accessToken?: string
}
export interface RedditSearchParams {
subreddit: string
query: string
sort?: 'relevance' | 'hot' | 'top' | 'new' | 'comments'
time?: 'hour' | 'day' | 'week' | 'month' | 'year' | 'all'
limit?: number
after?: string
before?: string
count?: number
show?: string
restrict_sr?: boolean
type?: 'link' | 'sr' | 'user'
sr_detail?: boolean
accessToken?: string
}
export interface RedditSubmitParams {
subreddit: string
title: string
text?: string
url?: string
nsfw?: boolean
spoiler?: boolean
send_replies?: boolean
flair_id?: string
flair_text?: string
collection_id?: string
accessToken?: string
}
export interface RedditVoteParams {
id: string
dir: 1 | 0 | -1
accessToken?: string
}
export interface RedditSaveParams {
id: string
category?: string
accessToken?: string
}
export interface RedditReplyParams {
parent_id: string
text: string
return_rtjson?: boolean
accessToken?: string
}
export interface RedditEditParams {
thing_id: string
text: string
accessToken?: string
}
export interface RedditDeleteParams {
id: string
accessToken?: string
}
export interface RedditSubscribeParams {
subreddit: string
action: 'sub' | 'unsub'
accessToken?: string
}
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
message?: string
data?: any
}
}
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
+19
View File
@@ -0,0 +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 {
const normalized = subreddit.trim().replace(SUBREDDIT_PREFIX, '')
const validation = validatePathSegment(normalized, { paramName: 'subreddit' })
if (!validation.isValid) {
throw new Error(validation.error)
}
return normalized
}
+101
View File
@@ -0,0 +1,101 @@
import type { RedditVoteParams, RedditWriteResponse } from '@/tools/reddit/types'
import type { ToolConfig } from '@/tools/types'
export const voteTool: ToolConfig<RedditVoteParams, RedditWriteResponse> = {
id: 'reddit_vote',
name: 'Vote on Reddit Post/Comment',
description: 'Upvote, downvote, or unvote a Reddit post or comment',
version: '1.0.0',
oauth: {
required: true,
provider: 'reddit',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Reddit API',
},
id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Thing fullname to vote on (e.g., "t3_abc123" for post, "t1_def456" for comment)',
},
dir: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Vote direction: 1 (upvote), 0 (unvote), or -1 (downvote)',
},
},
request: {
url: () => 'https://oauth.reddit.com/api/vote',
method: 'POST',
headers: (params: RedditVoteParams) => {
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: RedditVoteParams) => {
if (![1, 0, -1].includes(params.dir)) {
throw new Error('dir must be 1 (upvote), 0 (unvote), or -1 (downvote)')
}
const formData = new URLSearchParams({
id: params.id,
dir: params.dir.toString(),
})
return formData.toString()
},
},
transformResponse: async (response: Response, requestParams?: RedditVoteParams) => {
const data = await response.json().catch(() => ({}))
if (response.ok) {
const action =
requestParams?.dir === 1 ? 'upvoted' : requestParams?.dir === -1 ? 'downvoted' : 'unvoted'
return {
success: true,
output: {
success: true,
message: `Successfully ${action} ${requestParams?.id}`,
},
}
}
return {
success: false,
output: {
success: false,
message: 'Failed to vote',
data,
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the vote was successful',
},
message: {
type: 'string',
description: 'Success or error message',
},
},
}