|
1 | 1 | // lib/reddit.ts
|
2 | 2 |
|
3 | 3 | import axios from 'axios'
|
| 4 | +import Snoowrap from 'snoowrap' |
| 5 | +import { PrismaClient } from '@prisma/client' |
| 6 | +import { CommentStream } from 'snoostorm' |
4 | 7 |
|
| 8 | +const prisma = new PrismaClient() |
| 9 | + |
| 10 | +// Initialize Reddit API client with environment variables |
| 11 | +export const redditClient = new Snoowrap({ |
| 12 | + userAgent: 'your-app-name by /u/your-username', |
| 13 | + clientId: process.env.REDDIT_CLIENT_ID, |
| 14 | + clientSecret: process.env.REDDIT_CLIENT_SECRET, |
| 15 | + username: process.env.REDDIT_USERNAME, |
| 16 | + password: process.env.REDDIT_PASSWORD, |
| 17 | +}) |
| 18 | + |
| 19 | +/** |
| 20 | + * Stores Reddit messages/comments in database with deduplication |
| 21 | + * @param items Array of Reddit messages or comments |
| 22 | + * @returns Array of newly created database records |
| 23 | + */ |
| 24 | +export async function storeMessages(items: Array<Snoowrap.PrivateMessage | Snoowrap.Comment>) { |
| 25 | + const newMessages = [] |
| 26 | + console.debug(`Processing ${items.length} message(s)`) |
| 27 | + |
| 28 | + for (const item of items) { |
| 29 | + try { |
| 30 | + const isMessage = item instanceof Snoowrap.PrivateMessage |
| 31 | + const type = isMessage ? 'private_message' : 'comment' |
| 32 | + |
| 33 | + // Check for existing record |
| 34 | + const existing = await prisma.redditMessage.findUnique({ |
| 35 | + where: { redditId: item.name }, |
| 36 | + }) |
| 37 | + |
| 38 | + if (existing) { |
| 39 | + console.debug(`Skipping duplicate ${type} [${item.name}]`) |
| 40 | + continue |
| 41 | + } |
| 42 | + |
| 43 | + // Common fields for both messages and comments |
| 44 | + const baseData = { |
| 45 | + redditId: item.name, |
| 46 | + type, |
| 47 | + author: item.author.name, |
| 48 | + content: item.body, |
| 49 | + bodyHtml: item.body_html, |
| 50 | + subreddit: item.subreddit?.display_name, |
| 51 | + createdAt: new Date(item.created_utc * 1000), |
| 52 | + parentId: item.parent_id, |
| 53 | + rawData: item.toJSON(), |
| 54 | + } |
| 55 | + |
| 56 | + // Type-specific fields |
| 57 | + const additionalData = isMessage |
| 58 | + ? { |
| 59 | + isRead: item.new === false, |
| 60 | + contextUrl: item.context, |
| 61 | + } |
| 62 | + : { |
| 63 | + isRead: false, // Comments don't have a 'new' property in API response |
| 64 | + contextUrl: item.permalink, |
| 65 | + } |
| 66 | + |
| 67 | + const created = await prisma.redditMessage.create({ |
| 68 | + data: { ...baseData, ...additionalData }, |
| 69 | + }) |
| 70 | + |
| 71 | + console.debug(`Stored new ${type} [${created.redditId}] from /u/${created.author}`) |
| 72 | + newMessages.push(created) |
| 73 | + } catch (error) { |
| 74 | + console.error(`Error processing message ${item.name}:`, error.message) |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + return newMessages |
| 79 | +} |
| 80 | + |
| 81 | +/** |
| 82 | + * Fetches and processes Reddit inbox items |
| 83 | + * @returns Array of newly stored messages |
| 84 | + */ |
| 85 | +export async function checkRedditMessages() { |
| 86 | + try { |
| 87 | + console.debug('Fetching Reddit inbox...') |
| 88 | + |
| 89 | + // Parallel fetch of comments and messages |
| 90 | + const [commentReplies, messages] = await Promise.all([ |
| 91 | + redditClient.getInbox({ filter: 'comments' }), |
| 92 | + redditClient.getInbox({ filter: 'messages' }), |
| 93 | + ]) |
| 94 | + |
| 95 | + console.debug(`Found ${commentReplies.length} comment(s), ${messages.length} message(s)`) |
| 96 | + return await storeMessages([...commentReplies, ...messages]) |
| 97 | + } catch (error) { |
| 98 | + console.error('Reddit API Error:', error.message, error.stack) |
| 99 | + throw new Error(`Failed to fetch messages: ${error.message}`) |
| 100 | + } |
| 101 | +} |
| 102 | + |
| 103 | +/** |
| 104 | + * Retrieves unread messages from Reddit inbox |
| 105 | + * @returns Array of unread messages |
| 106 | + */ |
| 107 | +export const getUnreadMessages = async () => { |
| 108 | + console.debug('Fetching unread messages...') |
| 109 | + const messages = await redditClient.getUnreadMessages({ limit: 25 }) |
| 110 | + console.debug(`Found ${messages.length} unread message(s)`) |
| 111 | + return messages |
| 112 | +} |
| 113 | + |
| 114 | +/** |
| 115 | + * Marks a specific message as read |
| 116 | + * @param messageId Reddit message ID (fullname format, e.g. "t4_12345") |
| 117 | + */ |
| 118 | +export const markMessageRead = (messageId: string) => { |
| 119 | + try { |
| 120 | + console.debug(`Marking message ${messageId} as read...`) |
| 121 | + return redditClient.getMessage(messageId).markAsRead() |
| 122 | + } catch (error) { |
| 123 | + console.error(`Error marking message ${messageId} as read:`, error.message) |
| 124 | + } |
| 125 | +} |
| 126 | + |
| 127 | +/** |
| 128 | + * Fetches recent posts from specified subreddits |
| 129 | + * @param subreddits Array of subreddit names (without r/) |
| 130 | + * @returns Array of formatted post objects |
| 131 | + */ |
5 | 132 | export const fetchRedditPosts = async (subreddits: string[]) => {
|
6 | 133 | const posts = []
|
| 134 | + console.debug(`Fetching posts from ${subreddits.length} subreddit(s)`) |
7 | 135 |
|
8 | 136 | for (const subreddit of subreddits) {
|
9 |
| - const response = await axios.get(`https://www.reddit.com/r/${subreddit}/new.json?limit=10`) |
10 |
| - const subredditPosts = response.data.data.children.map((child: any) => ({ |
11 |
| - title: child.data.title, |
12 |
| - author: child.data.author, |
13 |
| - subreddit: child.data.subreddit, |
14 |
| - url: child.data.url, |
15 |
| - createdAt: new Date(child.data.created_utc * 1000), |
16 |
| - body: child.data.selftext, |
17 |
| - body_html: child.data.selftext_html, |
18 |
| - upvotes: child.data.ups, |
19 |
| - downvotes: child.data.downs, |
20 |
| - })) |
21 |
| - posts.push(...subredditPosts) |
| 137 | + try { |
| 138 | + console.debug(`Fetching /r/${subreddit}...`) |
| 139 | + const response = await axios.get(`https://www.reddit.com/r/${subreddit}/new.json?limit=10`, { |
| 140 | + timeout: 5000, |
| 141 | + }) |
| 142 | + |
| 143 | + const subredditPosts = response.data.data.children.map((child: any) => ({ |
| 144 | + title: child.data.title, |
| 145 | + author: child.data.author, |
| 146 | + subreddit: child.data.subreddit, |
| 147 | + url: child.data.url, |
| 148 | + createdAt: new Date(child.data.created_utc * 1000), |
| 149 | + body: child.data.selftext, |
| 150 | + bodyHtml: child.data.selftext_html, |
| 151 | + upvotes: child.data.ups, |
| 152 | + downvotes: child.data.downs, |
| 153 | + })) |
| 154 | + |
| 155 | + console.debug(`Found ${subredditPosts.length} post(s) in /r/${subreddit}`) |
| 156 | + posts.push(...subredditPosts) |
| 157 | + } catch (error) { |
| 158 | + console.error(`Error fetching /r/${subreddit}:`, error.message) |
| 159 | + } |
22 | 160 | }
|
23 | 161 |
|
24 | 162 | return posts
|
|
0 commit comments