diff --git a/apps/sim/blocks/blocks/reddit.ts b/apps/sim/blocks/blocks/reddit.ts index b901f4eed1..b82595d052 100644 --- a/apps/sim/blocks/blocks/reddit.ts +++ b/apps/sim/blocks/blocks/reddit.ts @@ -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', @@ -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 }, diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index 50577fd8ae..729d70ddf7 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -135,6 +135,7 @@ export const auth = betterAuth({ 'notion', 'microsoft', 'slack', + 'reddit', ], }, }, @@ -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, diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index 0a9499c709..7cf7257e9a 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -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(), diff --git a/apps/sim/lib/oauth/oauth.test.ts b/apps/sim/lib/oauth/oauth.test.ts index 16b37d428f..8689d05b04 100644 --- a/apps/sim/lib/oauth/oauth.test.ts +++ b/apps/sim/lib/oauth/oauth.test.ts @@ -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', }, })) @@ -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', + }, ] basicAuthProviders.forEach(({ name, providerId, endpoint }) => { diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 1513aa9369..34616d9edf 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -17,6 +17,7 @@ import { MicrosoftTeamsIcon, NotionIcon, OutlookIcon, + RedditIcon, SlackIcon, SupabaseIcon, xIcon, @@ -39,6 +40,7 @@ export type OAuthProvider = | 'microsoft' | 'linear' | 'slack' + | 'reddit' | string export type OAuthService = @@ -61,6 +63,7 @@ export type OAuthService = | 'outlook' | 'linear' | 'slack' + | 'reddit' export interface OAuthProviderConfig { id: OAuthProvider @@ -387,6 +390,23 @@ export const OAUTH_PROVIDERS: Record = { }, 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 @@ -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}`) } diff --git a/apps/sim/tools/reddit/get_comments.ts b/apps/sim/tools/reddit/get_comments.ts index 3c7c65035a..57632c97f6 100644 --- a/apps/sim/tools/reddit/get_comments.ts +++ b/apps/sim/tools/reddit/get_comments.ts @@ -7,6 +7,12 @@ export const getCommentsTool: ToolConfig ({ - '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) => { diff --git a/apps/sim/tools/reddit/get_posts.ts b/apps/sim/tools/reddit/get_posts.ts index 31bad6d5bf..00d5e9a34a 100644 --- a/apps/sim/tools/reddit/get_posts.ts +++ b/apps/sim/tools/reddit/get_posts.ts @@ -7,6 +7,12 @@ export const getPostsTool: ToolConfig = 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', @@ -38,8 +44,8 @@ export const getPostsTool: ToolConfig = 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) { @@ -49,29 +55,54 @@ export const getPostsTool: ToolConfig = 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 diff --git a/apps/sim/tools/reddit/hot_posts.ts b/apps/sim/tools/reddit/hot_posts.ts index 0d66d61d06..af49ec92a2 100644 --- a/apps/sim/tools/reddit/hot_posts.ts +++ b/apps/sim/tools/reddit/hot_posts.ts @@ -4,6 +4,7 @@ import type { RedditHotPostsResponse, RedditPost } from './types' interface HotPostsParams { subreddit: string limit?: number + accessToken: string } export const hotPostsTool: ToolConfig = { @@ -12,6 +13,12 @@ export const hotPostsTool: ToolConfig = 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', @@ -31,14 +38,20 @@ export const hotPostsTool: ToolConfig = 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) => { diff --git a/apps/sim/tools/reddit/types.ts b/apps/sim/tools/reddit/types.ts index 2a08abe989..b7f8323a18 100644 --- a/apps/sim/tools/reddit/types.ts +++ b/apps/sim/tools/reddit/types.ts @@ -39,6 +39,7 @@ export interface RedditPostsParams { sort?: 'hot' | 'new' | 'top' | 'rising' limit?: number time?: 'day' | 'week' | 'month' | 'year' | 'all' + accessToken?: string } // Response for the generalized get_posts tool @@ -55,6 +56,7 @@ export interface RedditCommentsParams { subreddit: string sort?: 'confidence' | 'top' | 'new' | 'controversial' | 'old' | 'random' | 'qa' limit?: number + accessToken?: string } // Response for the get_comments tool