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