fix(reddit): update to oauth endpoints (#627)

* fix(reddit): change tool to use oauth token

* fix lint

* add contact info

* Update apps/sim/tools/reddit/get_comments.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update apps/sim/tools/reddit/hot_posts.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update apps/sim/tools/reddit/get_posts.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix type error

---------

Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local>
Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Vikhyath Mondreti
2025-07-07 13:32:23 -07:00
committed by GitHub
parent c635b19548
commit 60e2e6c735
9 changed files with 198 additions and 31 deletions

View File

@@ -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 },

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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 }) => {

View File

@@ -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<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
@@ -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}`)
}

View File

@@ -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',
@@ -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) => {

View File

@@ -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',
@@ -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) {
@@ -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

View File

@@ -4,6 +4,7 @@ import type { RedditHotPostsResponse, RedditPost } from './types'
interface HotPostsParams {
subreddit: string
limit?: number
accessToken: string
}
export const hotPostsTool: ToolConfig<HotPostsParams, RedditHotPostsResponse> = {
@@ -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',
@@ -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) => {

View File

@@ -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