Skip to main content
Glama

Reddit MCP Server

by ozipi
reddit-post-service.ts•23.6 kB
/** * @file Reddit post and comment service * @module services/reddit/reddit-post-service * * @remarks * This service handles all post and comment-related operations for the Reddit API. * It extends RedditFetchService to inherit common fetching functionality and adds * specific methods for creating, fetching, and interacting with posts and comments. * * The service handles: * - Fetching posts with various sort options * - Creating new posts (text, link, image, video, gallery) * - Sending comments and replies * - Fetching individual posts and comments * - Managing notifications and messages * - Searching Reddit content * * @see {@link https://www.reddit.com/dev/api/#section_listings} Reddit Listings API * @see {@link https://www.reddit.com/dev/api/#section_links_and_comments} Reddit Links & Comments API */ import { DEFAULT_POST_LIMIT, DEFAULT_NOTIFICATION_LIMIT } from '../../constants.js'; import type { RedditMessageParams, RedditMessageResponse , RedditPost, RedditPostParams, RedditPostResponse, RedditApiResponse, FetchPostsOptions, RedditCommentThread, RedditPostWithComments, RedditNotification, FetchNotificationsOptions, RedditComment} from '../../types/reddit.js'; import { RedditError } from '../../types/reddit.js'; import { transformPost, transformComment, transformNotification, } from '../../utils/reddit-transformers.js'; import type { RedditAuthService } from './reddit-auth-service.js'; import { RedditFetchService } from './reddit-fetch-service.js'; /** * Service for handling Reddit post and comment operations * * @remarks * This service extends RedditFetchService to provide specialized methods * for working with Reddit posts, comments, and user interactions. * All methods include automatic rate limiting and error handling. * * @example * ```typescript * const postService = new RedditPostService( * 'https://oauth.reddit.com', * authService, * 2000 // 2 second rate limit * ); * * const posts = await postService.fetchPosts({ * subreddit: 'programming', * sort: 'hot', * limit: 25 * }); * ``` */ export class RedditPostService extends RedditFetchService { /** * Creates a new Reddit post service * * @param baseUrl - The base URL for Reddit API requests (https://oauth.reddit.com) * @param authService - The authentication service instance * @param rateLimitDelay - Delay in milliseconds between API requests */ constructor(baseUrl: string, authService: RedditAuthService, rateLimitDelay: number) { super(baseUrl, authService, rateLimitDelay); } /** * Fetches posts from Reddit based on the provided options * * @param options - Options for fetching posts * @param options.sort - Sort order: 'hot', 'new', or 'controversial' * @param options.limit - Maximum number of posts to fetch (default: 25) * @param options.subreddit - Subreddit to fetch from (optional) * @returns Array of Reddit posts * @throws {RedditError} Thrown if sort option is invalid or API request fails * * @example * ```typescript * // Fetch hot posts from a specific subreddit * const posts = await postService.fetchPosts({ * subreddit: 'typescript', * sort: 'hot', * limit: 50 * }); * * // Fetch new posts from home feed * const homePosts = await postService.fetchPosts({ * sort: 'new', * limit: 25 * }); * ``` */ public async fetchPosts( options: FetchPostsOptions = { sort: "hot", subreddit: "" }, ): Promise<RedditPost[]> { const { sort = "hot", limit = DEFAULT_POST_LIMIT, subreddit } = options; switch (sort) { case "hot": return this.getHotPosts(subreddit, limit); case "new": return this.getNewPosts(subreddit, limit); case "controversial": return this.getControversialPosts(subreddit, limit); default: throw new RedditError(`Invalid sort option: ${sort}`, "VALIDATION_ERROR"); } } /** * Formats a subreddit name by removing the r/ prefix if present * * @param subreddit - The subreddit name to format * @returns Formatted subreddit name without r/ prefix * @internal */ private formatSubreddit(subreddit?: string): string { if (!subreddit) {return "";} return subreddit.replace(/^r\//, ""); } /** * Fetches hot posts from Reddit * * @param subreddit - Optional subreddit to fetch from * @param limit - Maximum number of posts to fetch * @returns Array of hot posts * @throws {RedditError} Thrown if API request fails * @internal */ private async getHotPosts(subreddit?: string, limit: number = 10): Promise<RedditPost[]> { const formattedSubreddit = this.formatSubreddit(subreddit); const endpoint = formattedSubreddit ? `/r/${formattedSubreddit}/hot.json?limit=${limit}` : `/hot.json?limit=${limit}`; const data = await this.redditFetch<RedditApiResponse<RedditPost>>(endpoint); return data.data.children?.map((child) => transformPost(child.data)) ?? []; } /** * Fetches new posts from Reddit * * @param subreddit - Optional subreddit to fetch from * @param limit - Maximum number of posts to fetch * @returns Array of new posts sorted by creation time * @throws {RedditError} Thrown if API request fails * @internal */ private async getNewPosts(subreddit?: string, limit: number = 10): Promise<RedditPost[]> { const formattedSubreddit = this.formatSubreddit(subreddit); const endpoint = formattedSubreddit ? `/r/${formattedSubreddit}/new.json?limit=${limit}` : `/new.json?limit=${limit}`; const data = await this.redditFetch<RedditApiResponse<RedditPost>>(endpoint); return data.data.children?.map((child) => transformPost(child.data)) ?? []; } /** * Fetches controversial posts from Reddit * * @param subreddit - Optional subreddit to fetch from * @param limit - Maximum number of posts to fetch * @returns Array of controversial posts * @throws {RedditError} Thrown if API request fails * @internal */ private async getControversialPosts( subreddit?: string, limit: number = 10, ): Promise<RedditPost[]> { const formattedSubreddit = this.formatSubreddit(subreddit); const endpoint = formattedSubreddit ? `/r/${formattedSubreddit}/controversial.json?limit=${limit}` : `/controversial.json?limit=${limit}`; const data = await this.redditFetch<RedditApiResponse<RedditPost>>(endpoint); return data.data.children?.map((child) => transformPost(child.data)) ?? []; } /** * Creates a new post on Reddit * * @param params - Parameters for creating the post * @param params.subreddit - The subreddit to post in (without r/ prefix) * @param params.title - The post title (1-300 characters) * @param params.content - The post content (text/markdown) * @returns Response containing the created post details * @throws {RedditError} Thrown if required fields are missing or API request fails * * @remarks * This method currently only supports text posts. For link, image, video, * or gallery posts, additional parameters would need to be implemented. * * @example * ```typescript * const post = await postService.createPost({ * subreddit: 'test', * title: 'My First Post', * content: 'This is the post content' * }); * ``` */ public async createPost(params: RedditPostParams): Promise<RedditPostResponse> { const { subreddit, title, content } = params; if (!subreddit || !title || !content) { throw new RedditError("Missing required fields", "VALIDATION_ERROR"); } const formData = new URLSearchParams(); formData.append("sr", subreddit); formData.append("title", title); formData.append("text", content); const response = await this.redditFetch<RedditPostResponse>("/api/submit", { method: "POST", body: formData, }); return response; } /** * Fetches a single Reddit post by ID, including its comments * * @param id - The post ID (with or without t3_ prefix) * @returns The post with its comment tree * @throws {RedditError} Thrown if post not found or API request fails * * @remarks * This method fetches both the post metadata and its comment tree. * Comments are returned as a nested structure preserving the thread hierarchy. * * @example * ```typescript * const post = await postService.fetchPostById('abc123'); * console.log(post.title); * console.log(`${post.comments.length} top-level comments`); * * // Access nested replies * post.comments.forEach(thread => { * console.log(thread.comment.body); * console.log(`${thread.replies.length} replies`); * }); * ``` */ public async fetchPostById(id: string): Promise<RedditPostWithComments> { try { // Reddit API requires the post ID to be prefixed with t3_ const formattedId = id.startsWith("t3_") ? id : `t3_${id}`; // First, fetch the post info const postEndpoint = `/api/info.json?id=${formattedId}`; const postData = await this.redditFetch<RedditApiResponse<RedditPost>>(postEndpoint); if (!postData.data.children || postData.data.children.length === 0) { throw new RedditError(`Post with ID ${id} not found`, "API_ERROR"); } const post = transformPost(postData.data.children[0].data); // Then fetch the comments const rawid = id.replace("t3_", ""); const commentsEndpoint = `/comments/${rawid}.json`; const commentsData = await this.redditFetch<any[]>(commentsEndpoint); // Reddit returns an array with 2 elements: [0] = post data, [1] = comments data if (!commentsData || commentsData.length < 2) { // Return post without comments if comments data is missing return { ...post, comments: [], }; } // Process the comment tree const commentListing = commentsData[1].data.children; const comments = this.processCommentTree(commentListing); return { ...post, comments, }; } catch (error) { throw new RedditError( `Failed to fetch post: ${error instanceof Error ? error.message : "Unknown error"}`, "API_ERROR", error, ); } } /** * Recursively processes a Reddit comment tree into a structured format * * @param commentListing - Raw comment data from Reddit API * @returns Array of comment threads with nested replies * * @remarks * This method recursively processes the comment tree structure returned by Reddit. * Each comment can have replies, which can have their own replies, etc. * The method preserves this hierarchical structure. * * @internal */ private processCommentTree(commentListing: any[]): RedditCommentThread[] { if (!commentListing || !Array.isArray(commentListing)) { return []; } return commentListing .filter((item) => item.kind === "t1") .map((item) => { const comment = transformComment(item.data); // Process replies if they exist let replies: RedditCommentThread[] = []; if ( item.data.replies && typeof item.data.replies === "object" && item.data.replies.data && item.data.replies.data.children ) { replies = this.processCommentTree(item.data.replies.data.children); } return { comment, replies, }; }); } /** * Fetches user notifications from Reddit inbox * * @param options - Options for filtering and paginating notifications * @param options.filter - Filter type: 'all', 'unread', 'messages', 'comments', 'mentions' * @param options.limit - Maximum number of notifications to fetch (default: 25) * @param options.after - Pagination cursor for next page * @param options.before - Pagination cursor for previous page * @param options.markRead - Whether to mark fetched notifications as read * @returns Array of notifications * @throws {RedditError} Thrown if API request fails * * @example * ```typescript * // Get all unread notifications * const unread = await postService.fetchNotifications({ * filter: 'unread', * limit: 50 * }); * * // Get only comment replies * const comments = await postService.fetchNotifications({ * filter: 'comments', * limit: 25 * }); * ``` */ public async fetchNotifications( options: FetchNotificationsOptions = {}, ): Promise<RedditNotification[]> { try { const filter = options.filter || "all"; const limit = options.limit || DEFAULT_NOTIFICATION_LIMIT; // Determine the endpoint based on the filter let endpoint = ""; switch (filter) { case "unread": endpoint = "/message/unread.json"; break; case "messages": endpoint = "/message/messages.json"; break; case "comments": endpoint = "/message/comments.json"; break; case "mentions": endpoint = "/message/mentions.json"; break; case "all": default: endpoint = "/message/inbox.json"; break; } // Add parameters const params = new URLSearchParams(); params.append("limit", limit.toString()); if (options.after) {params.append("after", options.after);} if (options.before) {params.append("before", options.before);} endpoint += `?${params.toString()}`; const data = await this.redditFetch<RedditApiResponse<any>>(endpoint); if (!data.data.children) { return []; } let notifications = data.data.children.map((item) => transformNotification(item.data)); // Apply client-side filters if (options.excludeIds?.length) { notifications = notifications.filter((n) => !options.excludeIds?.includes(n.id)); } if (options.excludeTypes?.length) { notifications = notifications.filter((n) => !options.excludeTypes?.includes(n.type)); } if (options.excludeSubreddits?.length) { notifications = notifications.filter( (n) => !n.subreddit || !options.excludeSubreddits?.includes(n.subreddit), ); } if (options.markRead && notifications.length > 0 && filter !== "unread") { const ids = notifications.filter((n) => n.isNew).map((n) => n.id); if (ids.length > 0) { await this.markMessagesRead(ids); } } return notifications; } catch (error) { throw new RedditError( `Failed to fetch notifications: ${error instanceof Error ? error.message : "Unknown error"}`, "API_ERROR", error, ); } } public async deleteMessage(id: string): Promise<void> { try { const formData = new URLSearchParams({ id, }); await this.redditFetch("/api/del_msg", { method: "POST", body: formData, }); } catch (error) { throw new RedditError( `Failed to delete message: ${error instanceof Error ? error.message : "Unknown error"}`, "API_ERROR", error, ); } } private async markMessagesRead(ids: string[]): Promise<void> { const formData = new URLSearchParams({ id: ids.join(","), }); await this.redditFetch("/api/read_message", { method: "POST", body: formData, }); } /** * Fetches a single comment by its ID * @param id - The ID of the comment to fetch * @returns Promise<RedditComment> */ public async fetchCommentById(id: string): Promise<RedditComment> { try { // Reddit API requires the comment ID to be prefixed with t1_ const formattedId = id.startsWith("t1_") ? id : `t1_${id}`; const endpoint = `/api/info.json?id=${formattedId}`; const response = await this.redditFetch<RedditApiResponse<RedditComment>>(endpoint); if (!response.data.children || response.data.children.length === 0) { throw new RedditError(`Comment with ID ${id} not found`, "API_ERROR"); } return transformComment(response.data.children[0].data); } catch (error) { throw new RedditError( `Failed to fetch comment: ${error instanceof Error ? error.message : "Unknown error"}`, "API_ERROR", error, ); } } /** * Fetches a comment thread (comment with all its replies) by the comment ID and post ID * @param id - The ID of the post containing the comment * @param id - The ID of the comment to fetch with its replies * @returns Promise<RedditCommentThread> */ public async fetchCommentThread(parentId: string, id: string): Promise<RedditCommentThread> { try { // Reddit API endpoint for fetching a specific comment thread const endpoint = `/comments/${parentId.replace("t3_", "")}/comment/${id.replace("t1_", "")}.json`; const response = await this.redditFetch<any[]>(endpoint); if (!response || response.length < 2 || !response[1].data.children?.[0]) { throw new RedditError(`Comment thread not found`, "API_ERROR"); } // The first comment in the thread is our target comment const commentData = response[1].data.children[0]; if (commentData.kind !== "t1") { throw new RedditError(`Invalid comment data received`, "API_ERROR"); } const comment = transformComment(commentData.data); // Process replies if they exist let replies: RedditCommentThread[] = []; if ( commentData.data.replies && typeof commentData.data.replies === "object" && commentData.data.replies.data?.children ) { replies = this.processCommentTree(commentData.data.replies.data.children); } return { comment, replies, }; } catch (error) { throw new RedditError( `Failed to fetch comment thread: ${error instanceof Error ? error.message : "Unknown error"}`, "API_ERROR", error, ); } } public async searchReddit(options: { query: string; subreddit?: string; sort?: "relevance" | "hot" | "new" | "top"; time?: "hour" | "day" | "week" | "month" | "year" | "all"; limit?: number; }): Promise<RedditPost[]> { const { query, subreddit, sort = "relevance", time = "all", limit = DEFAULT_POST_LIMIT } = options; const formattedSubreddit = this.formatSubreddit(subreddit); let endpoint = formattedSubreddit ? `/r/${formattedSubreddit}/search.json` : "/search.json"; const params = new URLSearchParams({ q: query, sort, t: time, limit: limit.toString(), restrict_sr: formattedSubreddit ? "true" : "false", }); endpoint += `?${params.toString()}`; const data = await this.redditFetch<RedditApiResponse<RedditPost>>(endpoint); return data.data.children?.map((child) => transformPost(child.data)) ?? []; } /** * Sends a reply to a post or comment * @param id - The ID of the parent post or comment to reply to * @param text - The content of the reply * @returns Promise<any> - The API response */ public async sendReply(id: string, text: string): Promise<any> { try { const formData = new URLSearchParams({ parent: id, text: text, }); const response = await this.redditFetch("/api/comment", { method: "POST", body: formData, }); return response; } catch (error) { throw new RedditError( `Failed to send reply: ${error instanceof Error ? error.message : "Unknown error"}`, "API_ERROR", error, ); } } public async sendComment(id: string, text: string): Promise<any> { try { const response = await this.redditFetch<any>("/api/comment", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ parent_id: id, text, }).toString(), }); return { id: response.id, text: response.text, permalink: response.permalink, }; } catch (error) { throw error; } } /** * Sends a private message to another Reddit user * * @param params - Message parameters * @param params.recipient - Username of recipient (with or without u/ prefix) * @param params.subject - Message subject (max 100 characters) * @param params.content - Message body (max 10000 characters) * @returns Response containing sent message details * @throws {RedditError} Thrown if: * - Recipient is missing or invalid * - Reddit API returns errors * - API request fails * * @remarks * The method automatically: * - Cleans the recipient username (removes u/ prefix) * - Truncates subject to 100 characters * - Generates a unique message ID * - Handles Reddit API error responses * * @example * ```typescript * const message = await postService.sendMessage({ * recipient: 'username', * subject: 'Hello!', * content: 'This is a test message.' * }); * console.log(`Message sent with ID: ${message.id}`); * ``` */ public async sendMessage(params: RedditMessageParams): Promise<RedditMessageResponse> { try { const { recipient, subject, content } = params; if (!recipient) { throw new RedditError("Recipient is required", "VALIDATION_ERROR"); } // Remove any prefix (u/, /u/, etc) and trim whitespace const cleanRecipient = recipient.replace(/^(?:u\/|\/u\/)/i, "").trim(); if (!cleanRecipient) { throw new RedditError("Invalid recipient username", "VALIDATION_ERROR"); } // Create form data with proper encoding const formData = new URLSearchParams(); formData.append("api_type", "json"); formData.append("subject", subject.slice(0, 100)); formData.append("text", content); formData.append("to", cleanRecipient); const response = await this.redditFetch<{ json: { errors: Array<[string, string, string]>; // Reddit returns errors as [name, message, field] data?: { things: Array<{ data: { id: string; name: string; }; }>; }; }; }>("/api/compose", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: formData.toString(), }); // Check for errors in the response if (response.json?.errors && response.json.errors.length > 0) { const errorMessages = response.json.errors.map(([, message]) => message).join(", "); throw new RedditError(`Reddit API Error: ${errorMessages}`, "API_ERROR"); } // Generate a unique ID for the message since Reddit doesn't return one in a consistent format const messageId = `t4_${Date.now().toString(36)}`; return { id: messageId, recipient: cleanRecipient, subject, body: content, }; } catch (error) { if (error instanceof RedditError) { throw error; } throw new RedditError( `Failed to send message: ${error instanceof Error ? error.message : "Unknown error"}`, "API_ERROR", error, ); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ozipi/brainloop-mcp-server-v2'

If you have feedback or need assistance with the MCP directory API, please join our Discord server