diff --git a/src/index.ts b/src/index.ts index b7070fe..08f99bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,12 +4,9 @@ dotenv.config(); import WorkflowOrchestrator from './workflows/WorkflowOrchestrator'; import rDramaSession from './rdrama/session/SessionManager'; import redditSession from './reddit/session/SessionManager'; -import { CommentParser } from './rdrama/services/CommentParser'; import { DatabaseInitializer } from './db/initializeDatabase'; import { DatabaseService } from './db/services/Database'; import { DatabaseMaintenanceService } from './db/services/DatabaseMaintenance'; -import { CommentProcessor } from './rdrama/services/CommentProcessor'; -import { CommentPoster } from './rdrama/services/CommentPoster'; // Import other necessary services or configurations async function startApplication() { @@ -41,17 +38,8 @@ async function startApplication() { console.log('Reddit Session Start') await redditSession.getInstance() - // Initialize services with any required dependencies - const commentFetcher = new CommentProcessor(); - const commentParser = new CommentParser(); - const commentPoster = new CommentPoster() - // Initialize and start your workflow - const workflowOrchestrator = new WorkflowOrchestrator( - commentFetcher, - commentParser, - commentPoster - ); + const workflowOrchestrator = new WorkflowOrchestrator(); await workflowOrchestrator.executeWorkflow(); } diff --git a/src/rdrama/services/CommentParser.ts b/src/rdrama/services/CommentParser.ts index aa67a97..fa003c8 100644 --- a/src/rdrama/services/CommentParser.ts +++ b/src/rdrama/services/CommentParser.ts @@ -1,17 +1,16 @@ import { Comment } from '../models/Comment'; export class CommentParser { - private regexPattern: RegExp = /(^|\s|\\r\\n|\\t|[".,;(){}\[\]!?@#])(\/?u\/[a-zA-Z0-9_]+)/g; - /** * Extracts Reddit usernames from the body of a single comment. * @param comment A single Comment object to be processed. * @returns An array of unique Reddit usernames found in the comment. */ - public extractUsernames(comment: Comment): string[] { + public static extractUsernames(comment: Comment): string[] { + const regexPattern: RegExp = /(^|\s|\\r\\n|\\t|[".,;(){}\[\]!?@#])(\/?u\/[a-zA-Z0-9_]+)/g; const foundUsernames: Set = new Set(); - const matches = comment.body.match(this.regexPattern); + const matches = comment.body.match(regexPattern); if (matches) { matches.forEach(match => { // Ensure the username is captured in a standardized format diff --git a/src/rdrama/services/CommentPoster.ts b/src/rdrama/services/CommentPoster.ts index f10cb42..74c48fe 100644 --- a/src/rdrama/services/CommentPoster.ts +++ b/src/rdrama/services/CommentPoster.ts @@ -3,11 +3,6 @@ import { Comment } from '../models/Comment'; import FormData from 'form-data'; export class CommentPoster { - private sessionManager: SessionManager; - - constructor() { - this.sessionManager = SessionManager.getInstance(); - } /** * Posts a comment as a reply to a given rdrama comment. @@ -16,13 +11,14 @@ export class CommentPoster { * @param body The body of the comment to post. * @returns A promise resolving to the Axios response. */ - public async postComment(parentId: string, body: string): Promise { + public static async postComment(parentId: string, body: string): Promise { + const sessionManager = SessionManager.getInstance(); const formData = new FormData(); formData.append('parent_fullname', parentId); formData.append('body', body); try { - const response = await this.sessionManager.axiosInstance.post('/comment', formData, { + const response = await sessionManager.axiosInstance.post('/comment', formData, { headers: { 'Content-Type': 'multipart/form-data' } diff --git a/src/rdrama/services/CommentProcessor.ts b/src/rdrama/services/CommentProcessor.ts index 6729e5d..68952b9 100644 --- a/src/rdrama/services/CommentProcessor.ts +++ b/src/rdrama/services/CommentProcessor.ts @@ -8,16 +8,6 @@ import { CommentFetcher } from './CommentFetcher'; * with DatabaseService for checking the existence and persisting new comments. */ export class CommentProcessor { - private maxPages: number; - - /** - * Creates an instance of CommentProcessor. - * @param {DatabaseService} databaseService - The service for database operations. - * @param {number} maxPages - The maximum number of pages to fetch from the r/Drama API. Defaults to 10. - */ - constructor(maxPages: number = 10) { - this.maxPages = maxPages; - } /** * Fetches comments from the r/Drama API across multiple pages, up to the specified maximum. @@ -27,11 +17,11 @@ export class CommentProcessor { * @returns {Promise} A promise that resolves to an array of comments fetched from the API. * @throws {Error} Throws an error if there is a failure in fetching comments from the API. */ - async processComments(): Promise { + static async processComments(maxPages: number = 10): Promise { let comments: Comment[] = []; let stopFetching = false; - for (let page = 1; page <= this.maxPages && !stopFetching; page++) { + for (let page = 1; page <= maxPages && !stopFetching; page++) { const newComments = await CommentFetcher.fetchComments(page) // Check each new comment against the database and existing comments in this batch diff --git a/src/reddit/services/reddit.ts b/src/reddit/services/reddit.ts index 6ee9f3c..31ad666 100644 --- a/src/reddit/services/reddit.ts +++ b/src/reddit/services/reddit.ts @@ -1,17 +1,26 @@ -import axios, { AxiosError } from 'axios'; -import SessionManager from '../session/SessionManager'; import { RedditUser } from '../model/User'; +import RedditSessionManager from '../session/SessionManager'; +/** + * Provides services for interacting with Reddit user data and sending messages. + */ export class RedditService { - private sessionManager: SessionManager; - constructor(sessionManager: SessionManager) { - this.sessionManager = sessionManager; - } - - async getUserInfo(username: string): Promise { + /** + * Retrieves information about a Reddit user. + * + * @param {string} username - The username of the Reddit user to retrieve information for. + * @returns {Promise} A promise that resolves with the RedditUser object containing user information. + * @throws {Error} Throws an error if fetching user information fails. + * @example + * RedditService.getUserInfo('exampleUser') + * .then(userInfo => console.log(userInfo)) + * .catch(error => console.error(error)); + */ + static async getUserInfo(username: string): Promise { try { - const response = await this.sessionManager.axiosInstance.get(`/user/${username}/about`); + const redditSession = await RedditSessionManager.getInstance() + const response = await redditSession.axiosInstance.get(`/user/${username}/about`); return response.data; } catch (error) { console.error('Error fetching user info:', error); @@ -19,15 +28,37 @@ export class RedditService { } } - async sendMessage(username: string, subject: string, message: string): Promise { + /** + * Sends a private message to a Reddit user. + * + * @param {string} username - The username of the recipient Reddit user. + * @param {string} subject - The subject of the message. + * @param {string} message - The body text of the message. + * @returns {Promise} A promise that resolves when the message is successfully sent. + * @throws {Error} Throws an error if sending the message fails. + * @example + * RedditService.sendMessage('exampleUser', 'Hello', 'This is a test message.') + * .then(() => console.log('Message sent successfully.')) + * .catch(error => console.error('Error sending message:', error)); + */ + static async sendMessage(username: string, subject: string, message: string): Promise { try { - console.log(`await this.sessionManager.axiosInstance.post('/api/compose', {\n\tapi_type: 'json',\n\tto: ${username},\n\tsubject: ${subject},\n\ttext: ${message},\n});`) - //await this.sessionManager.axiosInstance.post('/api/compose', { - // api_type: 'json', - // to: username, - // subject: subject, - // text: message, - //}); + const redditSession = await RedditSessionManager.getInstance(); + // Create a URLSearchParams object with your data + const params = new URLSearchParams(); + params.append('api_type', 'json'); + params.append('to', `u/${username}`); + params.append('subject', subject); + params.append('text', message); + + // Use the params object directly in the data field + await redditSession.axiosInstance.post('/api/compose', params, { + headers: { + // Ensure the content type is set to application/x-www-form-urlencoded + 'Content-Type': 'application/x-www-form-urlencoded' + } + }); + console.log(JSON.stringify(params, null, 4)) console.log(`Message sent to ${username}`); } catch (error) { console.error('Error sending message:', error); diff --git a/src/utils/ShouldNotify.ts b/src/utils/ShouldNotify.ts index 6bcf1c6..725a2a0 100644 --- a/src/utils/ShouldNotify.ts +++ b/src/utils/ShouldNotify.ts @@ -5,8 +5,46 @@ import { DatabaseService } from '../db/services/Database'; // Load environment variables from .env file dotenv.config(); -export async function shouldNotifyUser(username: string, redditService: RedditService): Promise { - const userInfo = await redditService.getUserInfo(username); +/** + * Determines whether a user should be notified based on various criteria. + * + * This function checks a Reddit user's information against a set of conditions + * defined by environment variables and user attributes. These conditions include + * whether the user is a moderator, an employee, accepts private messages, has + * karma below a certain threshold, and has not been notified before. + * + * @param {string} username - The Reddit username of the user to check. + * @returns {Promise} - A promise that resolves to `true` if the user meets + * the criteria for notification, otherwise `false`. + * + * @throws {Error} Throws an error if there's a problem fetching user information + * from Reddit or checking the user's notification status in the database. + * + * @example + * // Example of checking if a user should be notified + * shouldNotifyUser('exampleUser').then(shouldNotify => { + * if (shouldNotify) { + * console.log('User should be notified.'); + * } else { + * console.log('User should not be notified.'); + * } + * }).catch(error => { + * console.error('Error checking if user should be notified:', error); + * }); + * + * @example + * // Environment variables used in this function + * // .env file + * EXCLUDE_MODS=true + * EXCLUDE_EMPLOYEES=true + * KARMA_THRESHOLD=100000 + * + * These environment variables control the behavior of the function, such as whether + * to exclude moderators or employees from notifications, and the karma threshold for + * notifications. + */ +export async function shouldNotifyUser(username: string): Promise { + const userInfo = await RedditService.getUserInfo(username); if (!userInfo) return false; const { is_mod, is_employee, accept_pms, total_karma } = userInfo.data; diff --git a/src/workflows/WorkflowOrchestrator.ts b/src/workflows/WorkflowOrchestrator.ts index cad7332..50505b7 100644 --- a/src/workflows/WorkflowOrchestrator.ts +++ b/src/workflows/WorkflowOrchestrator.ts @@ -4,71 +4,97 @@ import { CommentPoster } from "../rdrama/services/CommentPoster"; import { MessageService } from "../utils/MessageService"; import { DatabaseService } from "../db/services/Database"; import { RedditService } from "../reddit/services/Reddit"; -import RedditSessionManager from "../reddit/session/SessionManager"; import { shouldNotifyUser } from "../utils/ShouldNotify"; +import { Comment } from "../rdrama/models/Comment"; class WorkflowOrchestrator { - constructor( - private commentProcessor: CommentProcessor, - private commentParser: CommentParser, - private commentPoster: CommentPoster, - ) { } /** * Executes the defined workflow for processing comments. */ async executeWorkflow() { try { - // Fetch comments from the source - const comments = await this.commentProcessor.processComments(); - console.log(`Fetched ${comments.length} comments`); - - // Extract and deduplicate usernames from comments - const allUsernames = comments.flatMap(comment => this.commentParser.extractUsernames(comment)); - const uniqueUsernames = [...new Set(allUsernames)]; - console.log(`Extracted ${uniqueUsernames.length} unique usernames`); + const comments = await this.fetchAndLogComments(); for (const comment of comments) { - const redditUsers = this.commentParser.extractUsernames(comment) - if (redditUsers.length === 0) continue - console.log('found:', redditUsers) - const placeholdersRdrama = { - author_name: comment.author_name, - }; - for (const redditUser of redditUsers) { - const userMentionExists = await DatabaseService.userMentionExists(redditUser) - if (userMentionExists) continue - const commentResponseRdrama = MessageService.getRandomRdramaMessage(placeholdersRdrama) - if (!commentResponseRdrama) throw new Error('No comments for Rdrama found') - const postedComment = await this.commentPoster.postComment(`c_${comment.id}`, `##### TEST MESSAGE NO REDDITOR PINGED (YET...)\n${commentResponseRdrama}`) - //const postedComment = await this.commentPoster.postComment(`c_${comment.id}`, ${commentResponse}`) //TODO uncomment after golive - console.log(`Sent Comment to`, JSON.stringify(postedComment, null, 4)) - const redditSession = await RedditSessionManager.getInstance() - const redditService = new RedditService(redditSession) - const resultshouldNotifyUser = await shouldNotifyUser(redditUser, redditService) - if (!resultshouldNotifyUser) continue - const placeholdersReddit = { - author_name: comment.author_name, - username: redditUser, - permalink: comment.permalink - }; - const redditMessage = MessageService.getRandomRedditMessage(placeholdersReddit) - if (!redditMessage) throw new Error('No comments for Reddit found') - await DatabaseService.insertUserMention({ - rdrama_comment_id: comment.id, - username: redditUser, - message: redditMessage, - }) - await redditService.sendMessage(redditUser, 'Crosstalk PM Notification', redditMessage) - return; - - } + await this.processComment(comment); } + console.log('Workflow executed successfully.'); } catch (error) { console.error('An error occurred during workflow execution:', error); } } + + /** + * Fetches comments and logs the count. + * @returns {Promise} The fetched comments. + */ + async fetchAndLogComments(): Promise { + const comments = await CommentProcessor.processComments(); + console.log(`Fetched ${comments.length} comments`); + return comments; + } + + /** + * Processes a single comment, including posting responses and sending notifications. + * @param {Object} comment The comment to process. + */ + async processComment(comment: Comment) { + const redditUsers = CommentParser.extractUsernames(comment); + if (redditUsers.length === 0) return; + console.log('found:', redditUsers); + + for (const redditUser of redditUsers) { + await this.handleUserMention(comment, redditUser); + } + } + + /** + * Handles a mention of a user in a comment, including checking for previous mentions, posting a response, and sending a notification. + * @param {Object} comment The comment mentioning the user. + * @param {string} redditUser The mentioned Reddit user's username. + */ + async handleUserMention(comment: Comment, redditUser: string) { + const userMentionExists = await DatabaseService.userMentionExists(redditUser); + if (userMentionExists) return; + + const placeholdersRdrama = { author_name: comment.author_name }; + const commentResponseRdrama = MessageService.getRandomRdramaMessage(placeholdersRdrama); + if (!commentResponseRdrama) throw new Error('No comments for Rdrama found'); + + await this.postCommentAndNotify(comment, redditUser, commentResponseRdrama); + } + + /** + * Posts a comment response and sends a notification if the user should be notified. + * @param {Object} comment The original comment. + * @param {string} redditUser The Reddit user to notify. + * @param {string} commentResponseRdrama The response to post. + */ + async postCommentAndNotify(comment: Comment, redditUser: string, commentResponseRdrama: string) { + // Placeholder for posting a comment. Uncomment and implement as needed. + const postedComment = await CommentPoster.postComment(`c_${comment.id}`, `${commentResponseRdrama}`); + console.log(`Sent Comment to`, JSON.stringify(postedComment, null, 4)); + + const resultshouldNotifyUser = await shouldNotifyUser(redditUser); + if (!resultshouldNotifyUser) return; + + const placeholdersReddit = { + author_name: comment.author_name, + username: redditUser, + permalink: comment.permalink + }; + const redditMessage = MessageService.getRandomRedditMessage(placeholdersReddit); + if (!redditMessage) throw new Error('No comments for Reddit found'); + + await DatabaseService.insertUserMention({ + rdrama_comment_id: comment.id, + username: redditUser, + message: redditMessage, + }); + await RedditService.sendMessage(redditUser, 'Crosstalk PM Notification', redditMessage); + } } export default WorkflowOrchestrator; \ No newline at end of file