Skip to content
Open
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
8 changes: 8 additions & 0 deletions packages/core/src/types/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ export interface NodeCredentials {
refreshToken: string
expiresAt: number
}
/** Reddit OAuth2 credentials */
reddit?: {
clientId?: string
clientSecret?: string
accessToken?: string
refreshToken?: string
expiresAt?: number
}
}

/**
Expand Down
36 changes: 36 additions & 0 deletions packages/nodes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ export {
redditMonitorNode,
RedditMonitorInputSchema,
RedditMonitorOutputSchema,
redditCredential,
redditCreatePostNode,
RedditCreatePostInputSchema,
RedditCreatePostOutputSchema,
redditCreateCommentNode,
RedditCreateCommentInputSchema,
RedditCreateCommentOutputSchema,
redditReplyToCommentNode,
RedditReplyToCommentInputSchema,
RedditReplyToCommentOutputSchema,
redditSearchPostsNode,
RedditSearchPostsInputSchema,
RedditSearchPostsOutputSchema,
redditGetPostCommentsNode,
RedditGetPostCommentsInputSchema,
RedditGetPostCommentsOutputSchema,
twitterMonitorNode,
TwitterMonitorInputSchema,
TwitterMonitorOutputSchema,
Expand Down Expand Up @@ -202,6 +218,16 @@ export type {
RedditMonitorInput,
RedditMonitorOutput,
RedditPost,
RedditCreatePostInput,
RedditCreatePostOutput,
RedditCreateCommentInput,
RedditCreateCommentOutput,
RedditReplyToCommentInput,
RedditReplyToCommentOutput,
RedditSearchPostsInput,
RedditSearchPostsOutput,
RedditGetPostCommentsInput,
RedditGetPostCommentsOutput,
TwitterMonitorInput,
TwitterMonitorOutput,
TwitterPost,
Expand Down Expand Up @@ -315,6 +341,11 @@ import { mapNode, filterNode, sortNode } from './transform/index.js'
import { httpRequestNode, breadNode } from './examples/index.js'
import {
redditMonitorNode,
redditCreatePostNode,
redditCreateCommentNode,
redditReplyToCommentNode,
redditSearchPostsNode,
redditGetPostCommentsNode,
twitterMonitorNode,
twitterCreateTweetNode,
twitterDeleteTweetNode,
Expand Down Expand Up @@ -373,6 +404,11 @@ export const builtInNodes = [
breadNode,
// Integrations
redditMonitorNode,
redditCreatePostNode,
redditCreateCommentNode,
redditReplyToCommentNode,
redditSearchPostsNode,
redditGetPostCommentsNode,
twitterMonitorNode,
twitterCreateTweetNode,
twitterDeleteTweetNode,
Expand Down
26 changes: 26 additions & 0 deletions packages/nodes/src/integrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,32 @@ export {
type TwitterMonitorOutput,
type TwitterPost,
twitterCredential,
redditCredential,
redditCreatePostNode,
RedditCreatePostInputSchema,
RedditCreatePostOutputSchema,
type RedditCreatePostInput,
type RedditCreatePostOutput,
redditCreateCommentNode,
RedditCreateCommentInputSchema,
RedditCreateCommentOutputSchema,
type RedditCreateCommentInput,
type RedditCreateCommentOutput,
redditReplyToCommentNode,
RedditReplyToCommentInputSchema,
RedditReplyToCommentOutputSchema,
type RedditReplyToCommentInput,
type RedditReplyToCommentOutput,
redditSearchPostsNode,
RedditSearchPostsInputSchema,
RedditSearchPostsOutputSchema,
type RedditSearchPostsInput,
type RedditSearchPostsOutput,
redditGetPostCommentsNode,
RedditGetPostCommentsInputSchema,
RedditGetPostCommentsOutputSchema,
type RedditGetPostCommentsInput,
type RedditGetPostCommentsOutput,
twitterCreateTweetNode,
TwitterCreateTweetInputSchema,
TwitterCreateTweetOutputSchema,
Expand Down
18 changes: 18 additions & 0 deletions packages/nodes/src/integrations/social/credentials.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import { z } from 'zod';
import { defineOAuth2Credential } from '@jam-nodes/core';

export const redditCredential = defineOAuth2Credential({
name: 'reddit',
displayName: 'Reddit OAuth2',
documentationUrl: 'https://www.reddit.com/dev/api/',
config: {
authorizationUrl: 'https://www.reddit.com/api/v1/authorize',
tokenUrl: 'https://www.reddit.com/api/v1/access_token',
scopes: ['identity', 'submit', 'read', 'history', 'mysubreddits'],
},
schema: z.object({
clientId: z.string(),
clientSecret: z.string(),
accessToken: z.string(),
refreshToken: z.string().optional(),
expiresAt: z.number().optional(),
}),
});

export const twitterCredential = defineOAuth2Credential({
pkce: true,
name: 'twitter',
Expand Down
41 changes: 41 additions & 0 deletions packages/nodes/src/integrations/social/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,49 @@ export {

export {
twitterCredential,
redditCredential,
} from './credentials.js';

export {
redditCreatePostNode,
RedditCreatePostInputSchema,
RedditCreatePostOutputSchema,
type RedditCreatePostInput,
type RedditCreatePostOutput,
} from './reddit-create-post.js';

export {
redditCreateCommentNode,
RedditCreateCommentInputSchema,
RedditCreateCommentOutputSchema,
type RedditCreateCommentInput,
type RedditCreateCommentOutput,
} from './reddit-create-comment.js';

export {
redditReplyToCommentNode,
RedditReplyToCommentInputSchema,
RedditReplyToCommentOutputSchema,
type RedditReplyToCommentInput,
type RedditReplyToCommentOutput,
} from './reddit-reply-to-comment.js';

export {
redditSearchPostsNode,
RedditSearchPostsInputSchema,
RedditSearchPostsOutputSchema,
type RedditSearchPostsInput,
type RedditSearchPostsOutput,
} from './reddit-search-posts.js';

export {
redditGetPostCommentsNode,
RedditGetPostCommentsInputSchema,
RedditGetPostCommentsOutputSchema,
type RedditGetPostCommentsInput,
type RedditGetPostCommentsOutput,
} from './reddit-get-post-comments.js';

export {
twitterCreateTweetNode,
TwitterCreateTweetInputSchema,
Expand Down
64 changes: 64 additions & 0 deletions packages/nodes/src/integrations/social/reddit-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { NodeExecutionContext } from '@jam-nodes/core';
import { fetchWithRetry } from '../../utils/http.js';

const REDDIT_OAUTH_BASE_URL = 'https://oauth.reddit.com';
const REDDIT_USER_AGENT = 'jam-nodes/1.0';

export function getRedditAccessToken(context: NodeExecutionContext): string | null {
const creds = (context.credentials?.reddit || {}) as Record<string, string | undefined>;
return creds['accessToken'] || null;
}

export async function redditRequest<T>(
context: NodeExecutionContext,
path: string,
init: RequestInit = {}
): Promise<T> {
const accessToken = getRedditAccessToken(context);
if (!accessToken) {
throw new Error(
'Reddit access token not configured. Please provide context.credentials.reddit.accessToken.'
);
}

const fullUrl = `${REDDIT_OAUTH_BASE_URL}${path}`;

const response = await fetchWithRetry(
fullUrl,
{
...init,
headers: {
Authorization: `Bearer ${accessToken}`,
'User-Agent': REDDIT_USER_AGENT,
...(init.headers || {}),
},
},
{ maxRetries: 3, backoffMs: 1000, timeoutMs: 30000 }
);

if (!response.ok) {
const errorText = await response.text();
throw new Error(`Reddit API error: ${response.status} - ${errorText}`);
}

if (response.status === 204) {
return undefined as T;
}

return response.json() as Promise<T>;
}

export function formatRedditErrors(errors: unknown): string {
if (!Array.isArray(errors) || errors.length === 0) {
return 'Unknown Reddit API error';
}
return errors
.map((err) => {
if (Array.isArray(err)) {
return err.filter((part) => typeof part === 'string').join(': ');
}
if (typeof err === 'string') return err;
return JSON.stringify(err);
})
.join('; ');
}
114 changes: 114 additions & 0 deletions packages/nodes/src/integrations/social/reddit-create-comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { z } from 'zod';
import { defineNode } from '@jam-nodes/core';
import { getRedditAccessToken, redditRequest, formatRedditErrors } from './reddit-client.js';

interface RedditCommentResponse {
json?: {
errors?: unknown[];
data?: {
things?: Array<{
kind?: string;
data?: {
id?: string;
name?: string;
permalink?: string;
};
}>;
};
};
}

export const RedditCreateCommentInputSchema = z.object({
postId: z.string().min(1),
text: z.string().min(1),
});

export type RedditCreateCommentInput = z.infer<typeof RedditCreateCommentInputSchema>;

export const RedditCreateCommentOutputSchema = z.object({
commentId: z.string(),
commentName: z.string(),
permalink: z.string(),
});

export type RedditCreateCommentOutput = z.infer<typeof RedditCreateCommentOutputSchema>;

function normalizePostThingId(postId: string): string {
if (postId.startsWith('t3_') || postId.startsWith('t1_')) {
return postId;
}
return `t3_${postId}`;
}

export const redditCreateCommentNode = defineNode({
type: 'reddit_create_comment',
name: 'Reddit Create Comment',
description: 'Post a top-level comment on a Reddit post',
category: 'integration',
inputSchema: RedditCreateCommentInputSchema,
outputSchema: RedditCreateCommentOutputSchema,
estimatedDuration: 6,
capabilities: {
supportsRerun: true,
},
executor: async (input, context) => {
try {
if (!getRedditAccessToken(context)) {
return {
success: false,
error:
'Reddit access token not configured. Please provide context.credentials.reddit.accessToken.',
};
}

const body = new URLSearchParams();
body.set('api_type', 'json');
body.set('thing_id', normalizePostThingId(input.postId));
body.set('text', input.text);

const response = await redditRequest<RedditCommentResponse>(context, '/api/comment', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body.toString(),
});

const errors = response.json?.errors;
if (Array.isArray(errors) && errors.length > 0) {
return {
success: false,
error: `Reddit API error: ${formatRedditErrors(errors)}`,
};
}

const thing = response.json?.data?.things?.[0];
const commentId = thing?.data?.id;
const commentName = thing?.data?.name;
const rawPermalink = thing?.data?.permalink;

if (!commentId || !commentName) {
return {
success: false,
error: 'Reddit API returned an invalid create comment response.',
};
}

const permalink = rawPermalink ? `https://www.reddit.com${rawPermalink}` : '';

return {
success: true,
output: {
commentId,
commentName,
permalink,
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create Reddit comment',
};
}
},
});
Loading