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
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:
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user