Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions apps/sim/blocks/blocks/reddit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ export const RedditBlock: BlockConfig<
],
},

// Reddit OAuth Authentication
{
id: 'credential',
title: 'Reddit Account',
type: 'oauth-input',
layout: 'full',
provider: 'reddit',
serviceId: 'reddit',
requiredScopes: ['identity', 'read'],
placeholder: 'Select Reddit account',
},

// Common fields - appear for all actions
{
id: 'subreddit',
Expand Down Expand Up @@ -151,27 +163,31 @@ export const RedditBlock: BlockConfig<
},
params: (inputs) => {
const action = inputs.action || 'get_posts'
const { credential, ...rest } = inputs

if (action === 'get_comments') {
return {
postId: inputs.postId,
subreddit: inputs.subreddit,
sort: inputs.commentSort,
limit: inputs.commentLimit ? Number.parseInt(inputs.commentLimit) : undefined,
postId: rest.postId,
subreddit: rest.subreddit,
sort: rest.commentSort,
limit: rest.commentLimit ? Number.parseInt(rest.commentLimit) : undefined,
credential: credential,
}
}

return {
subreddit: inputs.subreddit,
sort: inputs.sort,
limit: inputs.limit ? Number.parseInt(inputs.limit) : undefined,
time: inputs.sort === 'top' ? inputs.time : undefined,
subreddit: rest.subreddit,
sort: rest.sort,
limit: rest.limit ? Number.parseInt(rest.limit) : undefined,
time: rest.sort === 'top' ? rest.time : undefined,
credential: credential,
}
},
},
},
inputs: {
action: { type: 'string', required: true },
credential: { type: 'string', required: true },
subreddit: { type: 'string', required: true },
sort: { type: 'string', required: true },
time: { type: 'string', required: false },
Expand Down
52 changes: 52 additions & 0 deletions apps/sim/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export const auth = betterAuth({
'notion',
'microsoft',
'slack',
'reddit',
],
},
},
Expand Down Expand Up @@ -825,6 +826,57 @@ export const auth = betterAuth({
},
},

// Reddit provider
{
providerId: 'reddit',
clientId: env.REDDIT_CLIENT_ID as string,
clientSecret: env.REDDIT_CLIENT_SECRET as string,
authorizationUrl: 'https://www.reddit.com/api/v1/authorize',
tokenUrl: 'https://www.reddit.com/api/v1/access_token',
userInfoUrl: 'https://oauth.reddit.com/api/v1/me',
scopes: ['identity', 'read'],
responseType: 'code',
pkce: false,
accessType: 'offline',
authentication: 'basic',
prompt: 'consent',
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/reddit`,
getUserInfo: async (tokens) => {
try {
const response = await fetch('https://oauth.reddit.com/api/v1/me', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
'User-Agent': 'sim-studio/1.0',
},
})

if (!response.ok) {
logger.error('Error fetching Reddit user info:', {
status: response.status,
statusText: response.statusText,
})
return null
}

const data = await response.json()
const now = new Date()

return {
id: data.id,
name: data.name || 'Reddit User',
email: `${data.name}@reddit.user`, // Reddit doesn't provide email in identity scope
image: data.icon_img || null,
emailVerified: false,
createdAt: now,
updatedAt: now,
}
} catch (error) {
logger.error('Error in Reddit getUserInfo:', { error })
return null
}
},
},

{
providerId: 'linear',
clientId: env.LINEAR_CLIENT_ID as string,
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ export const env = createEnv({
LINEAR_CLIENT_SECRET: z.string().optional(),
SLACK_CLIENT_ID: z.string().optional(),
SLACK_CLIENT_SECRET: z.string().optional(),
REDDIT_CLIENT_ID: z.string().optional(),
REDDIT_CLIENT_SECRET: z.string().optional(),
SOCKET_SERVER_URL: z.string().url().optional(),
SOCKET_PORT: z.number().optional(),
PORT: z.number().optional(),
Expand Down
7 changes: 7 additions & 0 deletions apps/sim/lib/oauth/oauth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ vi.mock('../env', () => ({
LINEAR_CLIENT_SECRET: 'linear_client_secret',
SLACK_CLIENT_ID: 'slack_client_id',
SLACK_CLIENT_SECRET: 'slack_client_secret',
REDDIT_CLIENT_ID: 'reddit_client_id',
REDDIT_CLIENT_SECRET: 'reddit_client_secret',
},
}))

Expand Down Expand Up @@ -80,6 +82,11 @@ describe('OAuth Token Refresh', () => {
endpoint: 'https://discord.com/api/v10/oauth2/token',
},
{ name: 'Linear', providerId: 'linear', endpoint: 'https://api.linear.app/oauth/token' },
{
name: 'Reddit',
providerId: 'reddit',
endpoint: 'https://www.reddit.com/api/v1/access_token',
},
Comment on lines +86 to +89
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider grouping Reddit with similar providers - Reddit's OAuth flow is more similar to GitHub's than Discord's, possibly should be in bodyCredentialProviders instead of basicAuthProviders

]

basicAuthProviders.forEach(({ name, providerId, endpoint }) => {
Expand Down
32 changes: 32 additions & 0 deletions apps/sim/lib/oauth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
MicrosoftTeamsIcon,
NotionIcon,
OutlookIcon,
RedditIcon,
SlackIcon,
SupabaseIcon,
xIcon,
Expand All @@ -39,6 +40,7 @@ export type OAuthProvider =
| 'microsoft'
| 'linear'
| 'slack'
| 'reddit'
| string

export type OAuthService =
Expand All @@ -61,6 +63,7 @@ export type OAuthService =
| 'outlook'
| 'linear'
| 'slack'
| 'reddit'

export interface OAuthProviderConfig {
id: OAuthProvider
Expand Down Expand Up @@ -387,6 +390,23 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
},
defaultService: 'slack',
},
reddit: {
id: 'reddit',
name: 'Reddit',
icon: (props) => RedditIcon(props),
services: {
reddit: {
id: 'reddit',
name: 'Reddit',
description: 'Access Reddit data and content from subreddits.',
providerId: 'reddit',
icon: (props) => RedditIcon(props),
baseProviderIcon: (props) => RedditIcon(props),
scopes: ['identity', 'read'],
},
},
defaultService: 'reddit',
},
}

// Helper function to get a service by provider and service ID
Expand Down Expand Up @@ -695,6 +715,18 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
useBasicAuth: false,
}
}
case 'reddit': {
const { clientId, clientSecret } = getCredentials(
env.REDDIT_CLIENT_ID,
env.REDDIT_CLIENT_SECRET
)
return {
tokenEndpoint: 'https://www.reddit.com/api/v1/access_token',
clientId,
clientSecret,
useBasicAuth: true,
}
}
default:
throw new Error(`Unsupported provider: ${provider}`)
}
Expand Down
26 changes: 19 additions & 7 deletions apps/sim/tools/reddit/get_comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export const getCommentsTool: ToolConfig<RedditCommentsParams, RedditCommentsRes
description: 'Fetch comments from a specific Reddit post',
version: '1.0.0',

oauth: {
required: true,
provider: 'reddit',
additionalScopes: ['read'],
},

params: {
postId: {
type: 'string',
Expand Down Expand Up @@ -38,15 +44,21 @@ export const getCommentsTool: ToolConfig<RedditCommentsParams, RedditCommentsRes
const sort = params.sort || 'confidence'
const limit = Math.min(Math.max(1, params.limit || 50), 100)

// Build URL
return `https://www.reddit.com/r/${subreddit}/comments/${params.postId}.json?sort=${sort}&limit=${limit}&raw_json=1`
// Build URL using OAuth endpoint
return `https://oauth.reddit.com/r/${subreddit}/comments/${params.postId}?sort=${sort}&limit=${limit}&raw_json=1`
},
method: 'GET',
headers: () => ({
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
Accept: 'application/json',
}),
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) => {
Expand Down
51 changes: 41 additions & 10 deletions apps/sim/tools/reddit/get_posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export const getPostsTool: ToolConfig<RedditPostsParams, RedditPostsResponse> =
description: 'Fetch posts from a subreddit with different sorting options',
version: '1.0.0',

oauth: {
required: true,
provider: 'reddit',
additionalScopes: ['read'],
},

params: {
subreddit: {
type: 'string',
Expand Down Expand Up @@ -38,8 +44,8 @@ export const getPostsTool: ToolConfig<RedditPostsParams, RedditPostsResponse> =
const sort = params.sort || 'hot'
const limit = Math.min(Math.max(1, params.limit || 10), 100)

// Build URL with appropriate parameters
let url = `https://www.reddit.com/r/${subreddit}/${sort}.json?limit=${limit}&raw_json=1`
// Build URL with appropriate parameters using OAuth endpoint
let url = `https://oauth.reddit.com/r/${subreddit}/${sort}?limit=${limit}&raw_json=1`

// Add time parameter only for 'top' sorting
if (sort === 'top' && params.time) {
Expand All @@ -49,29 +55,54 @@ export const getPostsTool: ToolConfig<RedditPostsParams, RedditPostsResponse> =
return url
},
method: 'GET',
headers: () => ({
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
Accept: 'application/json',
}),
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) => {
try {
// Check if response is OK
if (!response.ok) {
// Get response text for better error details
const errorText = await response.text()
console.error('Reddit API Error:', {
status: response.status,
statusText: response.statusText,
body: errorText,
url: response.url,
})

if (response.status === 403 || response.status === 429) {
throw new Error('Reddit API access blocked or rate limited. Please try again later.')
}
throw new Error(`Reddit API returned ${response.status}: ${response.statusText}`)
throw new Error(
`Reddit API returned ${response.status}: ${response.statusText}. Body: ${errorText}`
)
}

// Attempt to parse JSON
let data
try {
data = await response.json()
} catch (_error) {
throw new Error('Failed to parse Reddit API response: Response was not valid JSON')
} catch (error) {
const responseText = await response.text()
console.error('Failed to parse Reddit API response as JSON:', {
error: error instanceof Error ? error.message : String(error),
responseText,
contentType: response.headers.get('content-type'),
})
throw new Error(
`Failed to parse Reddit API response: Response was not valid JSON. Content: ${responseText}`
)
}

// Check if response contains error
Expand Down
25 changes: 19 additions & 6 deletions apps/sim/tools/reddit/hot_posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { RedditHotPostsResponse, RedditPost } from './types'
interface HotPostsParams {
subreddit: string
limit?: number
accessToken: string
}

export const hotPostsTool: ToolConfig<HotPostsParams, RedditHotPostsResponse> = {
Expand All @@ -12,6 +13,12 @@ export const hotPostsTool: ToolConfig<HotPostsParams, RedditHotPostsResponse> =
description: 'Fetch the most popular (hot) posts from a specified subreddit.',
version: '1.0.0',

oauth: {
required: true,
provider: 'reddit',
additionalScopes: ['read'],
},

params: {
subreddit: {
type: 'string',
Expand All @@ -31,14 +38,20 @@ export const hotPostsTool: ToolConfig<HotPostsParams, RedditHotPostsResponse> =
const subreddit = params.subreddit.trim().replace(/^r\//, '')
const limit = Math.min(Math.max(1, params.limit || 10), 100)

return `https://www.reddit.com/r/${subreddit}/hot.json?limit=${limit}&raw_json=1`
return `https://oauth.reddit.com/r/${subreddit}/hot?limit=${limit}&raw_json=1`
},
method: 'GET',
headers: () => ({
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
Accept: 'application/json',
}),
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) => {
Expand Down
Loading