UNPKG

systemprompt-mcp-reddit

Version:

A specialized Model Context Protocol (MCP) server that enables you to search, read, and interact with Reddit content, leveraging an AI Agent to help with each operation.

365 lines 15.5 kB
import { RedditError } from "../../types/reddit.js"; import { RedditFetchService } from "./reddit-fetch-service.js"; import { transformPost, transformComment, transformNotification, } from "../../utils/reddit-transformers.js"; export class RedditPostService extends RedditFetchService { constructor(baseUrl, authService, rateLimitDelay) { super(baseUrl, authService, rateLimitDelay); } async fetchPosts(options = { sort: "hot", subreddit: "" }) { const { sort = "hot", limit = 25, 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"); } } formatSubreddit(subreddit) { if (!subreddit) return ""; return subreddit.replace(/^r\//, ""); } async getHotPosts(subreddit, limit = 10) { const formattedSubreddit = this.formatSubreddit(subreddit); const endpoint = formattedSubreddit ? `/r/${formattedSubreddit}/hot.json?limit=${limit}` : `/hot.json?limit=${limit}`; const data = await this.redditFetch(endpoint); return data.data.children?.map((child) => transformPost(child.data)) ?? []; } async getNewPosts(subreddit, limit = 10) { const formattedSubreddit = this.formatSubreddit(subreddit); const endpoint = formattedSubreddit ? `/r/${formattedSubreddit}/new.json?limit=${limit}` : `/new.json?limit=${limit}`; const data = await this.redditFetch(endpoint); return data.data.children?.map((child) => transformPost(child.data)) ?? []; } async getControversialPosts(subreddit, limit = 10) { const formattedSubreddit = this.formatSubreddit(subreddit); const endpoint = formattedSubreddit ? `/r/${formattedSubreddit}/controversial.json?limit=${limit}` : `/controversial.json?limit=${limit}`; const data = await this.redditFetch(endpoint); return data.data.children?.map((child) => transformPost(child.data)) ?? []; } async createPost(params) { 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("/api/submit", { method: "POST", body: formData, }); return response; } async fetchPostById(id) { 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(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(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); } } processCommentTree(commentListing) { 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 = []; 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, }; }); } async fetchNotifications(options = {}) { try { const filter = options.filter || "all"; const limit = options.limit || 25; // 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(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); } } async deleteMessage(id) { 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); } } async markMessagesRead(ids) { 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> */ async fetchCommentById(id) { 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(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> */ async fetchCommentThread(parentId, id) { 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(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 = []; 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); } } async searchReddit(options) { const { query, subreddit, sort = "relevance", time = "all", limit = 25 } = 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(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 */ async sendReply(id, text) { 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); } } async sendComment(id, text) { try { const response = await this.redditFetch("/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; } } async sendMessage(params) { 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("/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); } } } //# sourceMappingURL=reddit-post-service.js.map