Compare commits

...

11 Commits

16 changed files with 664 additions and 412 deletions

View File

@ -1,24 +0,0 @@
CREATE TABLE IF NOT EXISTS comments (
db_id INTEGER PRIMARY KEY,
id INTEGER UNIQUE NOT NULL,
author_id INTEGER NOT NULL,
author_name TEXT NOT NULL,
body TEXT NOT NULL,
body_html TEXT NOT NULL,
created_utc INTEGER NOT NULL,
deleted_utc INTEGER DEFAULT 0,
distinguished BOOLEAN DEFAULT FALSE,
downvotes INTEGER DEFAULT 0,
edited_utc INTEGER DEFAULT 0,
is_banned BOOLEAN DEFAULT FALSE,
is_bot BOOLEAN DEFAULT FALSE,
is_nsfw BOOLEAN DEFAULT FALSE,
level INTEGER DEFAULT 0,
permalink TEXT NOT NULL,
pinned TEXT,
post_id INTEGER,
replies TEXT, -- Storing as JSON; consider relational integrity and querying needs.
reports TEXT, -- Storing as JSON; same considerations as 'replies'.
score INTEGER DEFAULT 0,
upvotes INTEGER DEFAULT 0
);

View File

@ -1,4 +0,0 @@
CREATE TABLE IF NOT EXISTS maintenance_log (
task_name TEXT PRIMARY KEY,
last_run TIMESTAMP NOT NULL
);

View File

@ -26,43 +26,6 @@ export class DatabaseService {
return db
}
/**
* Inserts a new comment into the database.
* This static method constructs an SQL statement to insert all fields of the Comment object
* into the corresponding columns in the 'comments' table.
*
* @example
* await DatabaseService.insertComment({
* id: 1,
* author_id: 123,
* author_name: 'exampleUser',
* body: 'This is a comment.',
* // More fields as per the Comment type
* });
*
* @param {Comment} comment - The comment object to insert.
* @throws {Error} Will throw an error if the insert operation fails.
*/
public static async insertComment(comment: Comment): Promise<void> {
const db = await DatabaseService.getDatabase()
const sql = `
INSERT INTO comments (
id, author_id, author_name, body, body_html, created_utc, deleted_utc,
distinguished, downvotes, edited_utc, is_banned, is_bot, is_nsfw, level,
permalink, pinned, post_id, replies, reports, score, upvotes
) VALUES (
?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?
)
`;
await db.run(sql, [
comment.id, comment.author_id, comment.author_name, comment.body, comment.body_html, comment.created_utc, comment.deleted_utc,
comment.distinguished ? 1 : 0, comment.downvotes, comment.edited_utc, comment.is_banned ? 1 : 0, comment.is_bot ? 1 : 0, comment.is_nsfw ? 1 : 0, comment.level,
comment.permalink, comment.pinned, comment.post_id, JSON.stringify(comment.replies), JSON.stringify(comment.reports), comment.score, comment.upvotes
]);
}
/**
* Inserts a new user mention into the database.
* This static method adds a record of a user being mentioned in a comment.
@ -86,24 +49,6 @@ export class DatabaseService {
await db.run(sql, [mention.rdrama_comment_id, mention.username, mention.message]);
}
/**
* Queries the database for an existing comment by its ID.
*
* @example
* const exists = await DatabaseService.commentExists('123');
* console.log(exists ? 'Comment exists.' : 'Comment does not exist.');
*
* @param {string} commentId - The ID of the comment to search for.
* @returns {Promise<boolean>} A boolean indicating whether the comment exists.
* @throws {Error} Will throw an error if the query operation fails.
*/
public static async commentExists(commentId: string): Promise<boolean> {
const db = await DatabaseService.getDatabase()
const sql = `SELECT 1 FROM comments WHERE id = ?`;
const result = await db.get(sql, [commentId]);
return !!result;
}
/**
* Queries the database to check if a username has been mentioned.
*
@ -122,60 +67,6 @@ export class DatabaseService {
return !!result;
}
/**
* Updates the last run timestamp for a maintenance task, using an "upsert" approach.
*
* @example
* await DatabaseService.updateLastRunTimestamp('purgeOldComments');
*
* @param {string} taskName - The name of the maintenance task.
* @throws {Error} Will throw an error if the update operation fails.
*/
public static async getLastRunTimestamp(taskName: string): Promise<Date | null> {
const db = await DatabaseService.getDatabase()
const result = await db.get(`SELECT last_run FROM maintenance_log WHERE task_name = ?`, [taskName]);
return result ? new Date(result.last_run) : null;
}
/**
* Updates the last run timestamp for a maintenance task, using an "upsert" approach.
*
* @example
* await DatabaseService.updateLastRunTimestamp('purgeOldComments');
*
* @param {string} taskName - The name of the maintenance task.
* @throws {Error} Will throw an error if the update operation fails.
*/
public static async updateLastRunTimestamp(taskName: string): Promise<void> {
// Assumes an "upsert" approach for the maintenance_log table
const db = await DatabaseService.getDatabase()
await db.run(
`INSERT INTO maintenance_log (task_name, last_run)
VALUES (?, ?)
ON CONFLICT(task_name)
DO UPDATE SET last_run = ?`,
[taskName, new Date(), new Date()]
);
}
/**
* Deletes comments from the database older than a specified number of days.
*
* @example
* await DatabaseService.purgeOldComments(30); // Purge comments older than 30 days
*
* @param {number} days - The age of comments to be purged, in days.
* @throws {Error} Will throw an error if the purge operation fails.
*/
public static async purgeOldComments(days: number = 1): Promise<void> {
const db = await DatabaseService.getDatabase()
console.log(`Purging comments older than ${days} days...`);
await db.run(`
DELETE FROM comments
WHERE datetime(created_utc, 'unixepoch') < datetime('now', '-${days} days')
`);
}
/**
* Inserts or updates the OAuth token in the database for a specific service.
*
@ -261,9 +152,9 @@ export class DatabaseService {
const lastNotificationTime = new Date(result.last_notification_time).getTime();
const currentTime = new Date(new Date().toISOString().slice(0, 19).replace('T', ' ')).getTime();
const timeElapsed = currentTime - lastNotificationTime;
console.log('timeElapsed', timeElapsed)
//console.log('timeElapsed', timeElapsed)
const cooldownPeriod = +cooldownHours * 60 * 60 * 1000; // Convert hours to milliseconds
console.log('cooldownPeriod', cooldownPeriod)
//console.log('cooldownPeriod', cooldownPeriod)
return timeElapsed >= cooldownPeriod;
}

View File

@ -1,69 +0,0 @@
import { DatabaseService } from './Database';
/**
* Manages and executes database maintenance tasks such as purging old comments.
* This service is responsible for periodically running maintenance tasks based on specified intervals.
*/
export class DatabaseMaintenanceService {
/**
* A list of maintenance tasks to be executed, each with a name, action, and interval.
*/
private maintenanceTasks = [
{
name: 'PurgeOldComments',
action: this.purgeOldComments.bind(this),
interval: 24 * 60 * 60 * 1000, // 24 hours in milliseconds
},
// Add more tasks here as needed
];
/**
* Executes all maintenance tasks that are due based on their defined intervals.
*/
public async runMaintenanceTasks() {
for (const task of this.maintenanceTasks) {
const shouldRun = await this.shouldRunTask(task.name, task.interval);
if (shouldRun) {
await task.action();
await this.updateLastRunTimestamp(task.name);
}
}
}
/**
* Determines whether a specific maintenance task should run based on its last execution time and defined interval.
*
* @param {string} taskName - The name of the task to check.
* @param {number} interval - The interval in milliseconds to determine if the task should run.
* @returns {Promise<boolean>} True if the task should run, otherwise false.
*/
private async shouldRunTask(taskName: string, interval: number): Promise<boolean> {
// Use the DatabaseService to check the last run timestamp from the maintenance_log table
const lastRun = await DatabaseService.getLastRunTimestamp(taskName);
if (!lastRun) return true; // Task has never run
const now = Date.now();
return (now - lastRun.getTime()) > interval;
}
/**
* Purges old comments from the database.
*/
private async purgeOldComments() {
console.log("Purging old comments...");
// Use the DatabaseService for the SQL operation
await DatabaseService.purgeOldComments();
}
/**
* Updates the last run timestamp for a specific maintenance task.
*
* @param {string} taskName - The name of the task for which to update the last run timestamp.
*/
private async updateLastRunTimestamp(taskName: string) {
// Use the DatabaseService to update the last run timestamp in the maintenance_log table
await DatabaseService.updateLastRunTimestamp(taskName);
}
}

View File

@ -5,8 +5,9 @@ import WorkflowOrchestrator from './workflows/WorkflowOrchestrator';
import rDramaSession from './rdrama/session/SessionManager';
import redditSession from './reddit/session/SessionManager';
import { DatabaseInitializer } from './db/initializeDatabase';
import { DatabaseService } from './db/services/Database';
import { DatabaseMaintenanceService } from './db/services/DatabaseMaintenance';
import RedisSessionManager from './redis/session/SessionManager';
import { Comment } from './rdrama/models/Comment';
// Import other necessary services or configurations
async function startApplication() {
@ -16,12 +17,6 @@ async function startApplication() {
if (!db) {
throw new Error('Failed to initialize the database.');
}
const canSend = await DatabaseService.canSendNotification();
const coolDownHours = process.env.NOTIFICATION_COOLDOWN_HOURS
if (!canSend) {
console.log(`Last Message Sent less than ${coolDownHours ? coolDownHours : 4} hours ago. Set NOTIFICATION_COOLDOWN_HOURS to change this`)
return;
}
console.log('RDrama Session Start')
// Initialize SessionManager or other global configurations
@ -31,18 +26,60 @@ async function startApplication() {
}
rDramaSessionManager.setAuthorizationToken(process.env.RDRAMA_API_KEY);
console.log('Database Maintenance Start')
const databaseMaintenance = new DatabaseMaintenanceService()
await databaseMaintenance.runMaintenanceTasks()
console.log('Redis Start');
const redisManager = RedisSessionManager.getInstance();
redisManager.client.subscribe('newCommentsChannel', (error, count) => {
if (error) throw new Error('Failed to subscribe to Redis channel.');
console.log(`Subscribed to ${'newCommentsChannel'}. Listening for new comments.`);
});
console.log('Reddit Session Start')
await redditSession.getInstance()
const redditSessionManager = await redditSession.getInstance()
// Initialize and start your workflow
const workflowOrchestrator = new WorkflowOrchestrator();
await workflowOrchestrator.executeWorkflow();
await rDramaSessionManager.shutdown()
redisManager.client.on('message', async (channel, message) => {
if (channel === 'newCommentsChannel') {
const comment: Comment = JSON.parse(message)
console.log(`New comment ${comment.id} received.`);
await workflowOrchestrator.executeWorkflow(comment);
}
});
setupProcessListeners(redisManager, redditSessionManager, rDramaSessionManager);
}
function setupProcessListeners(redisManager: RedisSessionManager, redditSessionManager: redditSession, rDramaSessionManager: rDramaSession) {
process.on('SIGINT', async () => {
console.log('SIGINT received. Shutting down gracefully...');
await shutdownServices(redisManager, redditSessionManager, rDramaSessionManager);
});
process.on('SIGTERM', async () => {
console.log('SIGTERM received. Shutting down gracefully...');
await shutdownServices(redisManager, redditSessionManager, rDramaSessionManager);
});
process.on('uncaughtException', async (error) => {
console.error('Uncaught Exception:', error);
await shutdownServices(redisManager, redditSessionManager, rDramaSessionManager);
process.exit(1); // Exit with error
});
process.on('unhandledRejection', async (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
await shutdownServices(redisManager, redditSessionManager, rDramaSessionManager);
process.exit(1); // Exit with error
});
}
async function shutdownServices(redisManager: RedisSessionManager, redditSessionManager: redditSession, rDramaSessionManager: rDramaSession) {
await redisManager.shutdown();
await redditSessionManager.shutdown();
await rDramaSessionManager.shutdown();
console.log('Services shut down successfully.');
process.exit(0); // Exit cleanly
}
startApplication()

View File

@ -0,0 +1,183 @@
Hi {author_name},
We noticed your recent mention of a Reddit user in your post/comment. While fostering a community built on respect and privacy, we wanted to inform you that the mentioned user has already been notified of a similar mention before. We didn't send another notification to respect their inbox. Please continue to consider the implications of mentioning users in the future.
Thank you for helping us maintain a respectful community.
Warm regards,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hello {author_name},
Your recent action involving the mention of a Reddit user caught our attention. Our community values privacy and respectful discourse above all. Given that the mentioned individual has previously been notified of a mention, we have chosen not to send another notification this time. We encourage you to reflect on the privacy and respect of others in all your interactions.
Best wishes,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Greetings {author_name},
We're writing to you about your mention of a Reddit user in your communication. As part of our commitment to privacy and respectful interactions, we wanted to let you know that the individual you mentioned has already been informed about a previous mention. To avoid redundant notifications, we did not send an additional message. We kindly ask you to keep the potential impact of your mentions in mind for future posts and comments.
Thank you for your understanding,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Dear {author_name},
We observed your mention of a Reddit user in your recent post/comment. Our community cherishes the principles of privacy and respect. The user you mentioned has been previously notified about a mention. To prevent overwhelming them, we've opted not to send a second notification. Your mindfulness in future mentions is greatly appreciated.
Kind regards,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hey {author_name},
Just a quick heads-up about your mention of a Reddit user. We're all about keeping things respectful and private here. The mentioned user has been notified before, so we skipped sending another alert to keep their notifications tidy. Let's continue to be thoughtful about our mentions, shall we?
Cheers,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hello {author_name},
Your recent mention of a Reddit user has been noted. Our aim is to foster a space of respect and privacy. Since the mentioned user has previously received a notification, we refrained from sending another to avoid repetition. Please be considerate of the impacts of your mentions going forward.
With appreciation,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hi there, {author_name},
We caught your mention of a Reddit user in your post/comment. As a friendly reminder, our community values privacy and kindness. The user mentioned has already been alerted once, so we decided not to send a repeat notification. Thanks for understanding and for your cooperation in maintaining a positive space.
Warm wishes,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hello {author_name},
Regarding your mention of a Reddit user: we're dedicated to respectful and private interactions. The user has been notified previously, hence we did not send an additional message this time. It's all part of ensuring everyone's experience remains pleasant. Thank you for playing your part!
Best,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Dear {author_name},
We noticed your mention of a Reddit user. In line with our respect for privacy, we want to inform you that the mentioned user has been previously notified, so we've refrained from sending another message. Your awareness and discretion in future mentions are much appreciated.
Kindly,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hey {author_name},
Saw your mention of a Reddit user! Just so you know, we've already notified them about a past mention, so we didn't send another one to keep things chill. Let's keep being awesome and respectful in our mentions, okay?
Thanks a bunch,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Greetings {author_name},
In your recent post/comment, you mentioned a Reddit user. We value our community's privacy and have previously informed the user about a mention. To avoid notification overload, we opted out of sending another. Your understanding and respect for privacy are appreciated.
Regards,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hello {author_name},
Your mention of a Reddit user was noted. Since they've been notified previously, we chose not to send a duplicate notification, aligning with our community's values of respect and discretion. We trust you understand and will continue to consider these values in your future engagements.
Sincerely,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hi {author_name},
You mentioned a Reddit user recently. To maintain a respectful atmosphere and prevent redundancy, we didn't send another notification since they were already informed once before. Your help in keeping our community considerate and respectful is invaluable.
Thank you,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Dear {author_name},
We've observed your recent activity involving a mention of a Reddit user. To maintain our community's commitment to discretion and respect, we've chosen not to resend a notification to the mentioned user, as they have already been contacted. We appreciate your understanding and your contributions to a respectful community atmosphere.
Best regards,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hello {author_name},
Your recent mention of a Reddit user caught our attention. In line with our dedication to privacy and respect, we've decided against sending another notification to avoid duplicity. Your continued mindfulness and consideration in interactions are what make our community great. Thank you for your cooperation.
Cheers,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hi {author_name},
Thanks for your engagement in our community. Regarding your mention of a Reddit user: they've already been made aware of a previous mention. To prevent notification fatigue, we've not sent another alert. Your understanding and adherence to our community values are greatly appreciated.
Kindly,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Greetings {author_name},
We noticed your mention of a Reddit user and wanted to remind you of the importance of respect and privacy within our community. As the mentioned user has previously been notified, we have refrained from sending an additional message. Your thoughtful participation helps us maintain a positive community environment.
Warm regards,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Dear {author_name},
Your action of mentioning a Reddit user has been noted. Our policy is to keep our community respectful and private, so we've chosen not to resend a notification to the mentioned user, who has already been informed once. We're thankful for your understanding and for helping us preserve a respectful space.
Sincerely,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hello {author_name},
In your recent contribution, you mentioned a Reddit user. Our community principles guide us to avoid over-notifying users who've already been mentioned, to respect their privacy. Therefore, we did not send an additional notification. Your cooperation in these matters is highly valued.
Thanks,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hey {author_name},
Just touching base about your recent mention of a Reddit user. To keep our community vibe positive and respectful, we've held off on sending another notification to the user you mentioned since they've been notified before. Thanks for helping us keep the peace and privacy!
All the best,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Dear {author_name},
We've seen your mention of a Reddit user in your recent post/comment. Our community ethos of privacy and respect means we've opted not to send another notification, as the user has been previously informed. Your understanding and respect in this matter help us maintain a healthy community dynamic.
Regards,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hello {author_name},
Your mention of a Reddit user has been registered. In keeping with our commitment to privacy, we've chosen not to issue another notification to them, considering they've been alerted before. We trust you appreciate our discretion and thank you for your thoughtful participation in our community.
Warm wishes,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hi {author_name},
We noticed your recent mention of a Reddit user. Our aim is to respect everyone's privacy and avoid notification overload, so we didn't send another alert to the mentioned user, who has been previously notified. Thanks for understanding and for being a valued member of our respectful community.
Take care,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---

View File

@ -0,0 +1,231 @@
Hi {author_name},
Thanks for engaging within our community. We noticed your mention of a Reddit user who's quite the connoisseur of Reddit's ways. Their rich history and karma speak to their deep engagement, so we've opted not to send a notification. Your consideration in these matters is invaluable.
Kindly,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hey {author_name},
We caught your mention of a user who's no stranger to the Reddit scene. Their impressive karma and platform wisdom mean we'll skip the notification this time. Thanks for being a part of our respectful and privacy-conscious community.
All the best,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hello {author_name},
Your recent reference to a Reddit stalwart caught our attention. Given their storied journey and significant karma, we've decided it's best to hold back on sending them another ping. Your engagement is what makes our community vibrant.
Regards,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Greetings {author_name},
We've noticed you mentioned a user deeply woven into the fabric of Reddit. Their experience and karma levels suggest they're already adept at navigating the platform's nuances. To avoid overburdening them, we won't be sending a notification. Thanks for helping maintain a considerate environment.
Best,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Dear {author_name},
Your mention of a seasoned Reddit navigator has been duly noted. Their extensive karma and active participation highlight their expertise. As such, we believe an additional notification might not be necessary. We appreciate your efforts in cultivating a thoughtful community.
Warmly,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hey {author_name},
You've mentioned a Reddit veteran, someone well-versed in the ebb and flow of this vast platform. Considering their significant karma and contributions, we're skipping the notification step this round. Your mindfulness is what makes this community special.
Thanks,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hi {author_name},
It seems you've mentioned a Reddit user who's quite familiar with the platform's ins and outs, judging by their high karma and active engagement. We'll forgo the notification to respect their seasoned user status. Your thoughtful interaction is what keeps our community thriving.
Cheers,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hello {author_name},
Your reference to a Reddit user known for their extensive engagement and high karma has been recorded. In recognition of their profound experience, we'll hold off on sending a notification. Your sensitivity to community dynamics is truly valued.
Thank you,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Greetings {author_name},
We've seen your mention of a user who's no newbie to Reddit's corridors. Their impressive karma tally and history suggest they're well-equipped to handle mentions, so we'll skip the extra notification. Thank you for contributing to a respectful community.
Sincerely,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Dear {author_name},
Your recent activity brought to light a mention of a Reddit user whose karma and historical engagement are nothing short of remarkable. With respect to their seasoned presence, we see no need for an additional notification. Your participation in fostering a considerate community is greatly appreciated.
Best wishes,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hey {author_name},
You've highlighted a user who's a Reddit aficionado, someone with a karma score and engagement level that speaks volumes. We'll hold off on notifying them this time, trusting in their seasoned approach to community interactions. Thanks for being considerate and respectful.
Take care,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hi {author_name},
Your engagement has led to the mention of a Reddit user whose journey on the platform is both extensive and impressive. Given their high karma and active participation, we'll refrain from sending a notification. Your thoughtful presence is a boon to our community.
With gratitude,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Dear {author_name},
Your shoutout to a seasoned Reddit user caught our eye. With their wealth of experience and karma, they're well-equipped to navigate mentions. To honor their established presence, we're holding back on sending a notification this time. Your understanding enhances our community's respectfulness.
Sincerely,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hello {author_name},
You've highlighted a Reddit veteran in your recent post. Their journey through Reddit's corridors, marked by significant karma, suggests they're adept at managing interactions. We're skipping the notification to acknowledge their seasoned expertise. Thanks for your thoughtful engagement.
Best,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Greetings {author_name},
We noticed you mentioned a Reddit luminary. Given their extensive karma and notable contributions, we believe they're more than capable of handling mentions independently. In respect of their veteran status, no notification will be sent. Your sensitivity to these nuances is much appreciated.
Warmly,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hi there, {author_name},
Your recent nod to a Reddit heavyweight was observed. Their towering karma and active participation speak volumes of their adeptness on the platform. Recognizing their capability, we've decided against a notification. We value your contribution to our thoughtful community.
Cheers,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Dear {author_name},
You've referenced a Reddit stalwart in your dialogue. Their profound karma and enduring presence underscore a comprehensive Reddit experience. To honor their adept handling of such mentions, no additional notification will be dispatched. We're grateful for your discerning interaction.
Regards,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hello {author_name},
Your mention of a Reddit sage has been noted. Their extensive karma accumulation and insightful contributions signify a seasoned journey on Reddit. Acknowledging their familiarity with community dynamics, we're omitting the notification. Thank you for promoting respectful discourse.
Best wishes,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hey {author_name},
Your callout to a Reddit old-timer didn't go unnoticed. Their enviable karma score and history of engagement demonstrate their thorough Reddit literacy. In deference to their seasoned insight, we'll forego the notification. Your mindfulness is what makes our community special.
Take care,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Dear {author_name},
In your recent post, you've acknowledged a Reddit authority. Their rich karma and history of insightful interactions highlight a profound engagement with the Reddit community. Recognizing their adeptness, we've opted out of sending a notification. Your respect for community pillars is commendable.
Kind regards,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hello {author_name},
We've taken note of your homage to a Reddit icon. With a karma score that's through the roof and countless contributions, they epitomize the essence of Reddit. This time, we'll hold back on the notification to honor their mastery. We thank you for your understanding and respectful engagement.
Sincerely,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hi {author_name},
Your pointer towards a Reddit legend has been appreciated. Their stellar karma and active presence are a testament to their Reddit savviness. To respect their well-earned stature, no notification will be sent. Your actions contribute greatly to our community's positive ethos.
Warmest regards,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Greetings, {author_name},
You've mentioned a Reddit veteran whose contributions have already painted them as a notable figure. Recognizing their seasoned presence, we're holding back on the notification. It's the shared understanding and respect that keep our community thriving.
Cheers,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hello {author_name},
Your shoutout to a user well-versed in the Reddit realm caught our eye. Given their established rapport and karma wealth, we'll forgo the alert. It's contributors like you that make our community a richer place.
With gratitude,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Dear {author_name},
We observed your nod to an esteemed Reddit user. Their journey and karma milestones suggest they're well acquainted with the ebb and flow here, so we've decided not to dispatch a notification. Thanks for helping maintain a courteous environment.
Sincerely,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hi there, {author_name},
A tip of the hat to a distinguished Reddit user, we see! Their legacy and karma scores are testament enough, thus negating the need for further notice. Your actions reinforce the mutual respect that underpins our community.
Warmly,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hey {author_name},
A shoutout to a seasoned Redditor, eh? Their karma and tenure are indicative of a well-trodden path here, sparing us the need to send out a notification. It's this thoughtful engagement that cultivates our community's spirit.
Appreciatively,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Good day, {author_name},
Your recent mention of a Reddit stalwart didn't go unnoticed. With their karma echoing years of engagement, we'll refrain from sending a notice. It's the understanding and respect you show that keeps our community's foundation strong.
Best,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Salutations, {author_name},
Recognizing a Reddit luminary, we noted your mention. Their vast karma and contributions make further notification redundant. Your mindfulness in community interactions is truly commendable.
Regards,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hello again, {author_name},
You've acknowledged a Reddit user who's practically a legend here, given their karma and activity. In light of their veteran status, we see no need for an additional notification. Your insight into community dynamics is appreciated.
Take care,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Hi {author_name},
You've tipped your hat to a user whose Reddit karma and history speak volumes. As such, we're bypassing the notification step. Your keen sense of community etiquette doesn't go unnoticed.
Thank you,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---
Greetings, {author_name},
Your mention of a Reddit user known for their extensive karma and platform engagement caught our attention. Given their familiarity with the Reddit ecosystem, we'll omit the notification. Your thoughtful participation helps shape a respectful community.
Warm regards,
CrossTalk PM - Automated Message (Unmonitored Account)
---END---

View File

@ -1,30 +0,0 @@
import SessionManager from '../session/SessionManager';
import { Comment } from '../models/Comment';
/**
* CommentFetcher is responsible for fetching comments from the r/Drama API.
* It utilizes a SessionManager instance to manage API requests with appropriate
* headers and configurations, including rate limiting and retries.
*/
export class CommentFetcher {
/**
* Fetches comments from the r/Drama API across multiple pages.
* Each page's fetch operation is timed for performance analysis.
*
* @returns {Promise<Comment[]>} 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.
*/
static async fetchComments(page: number): Promise<Comment[]> {
console.time(`Fetching page: ${page}`);
try {
const axiosInstance = SessionManager.getInstance().axiosInstance;
const response = await axiosInstance.get(`/comments?page=${page}`);
console.timeEnd(`Fetching page: ${page}`);
return response.data.data;
} catch (error) {
console.error(`Failed to fetch comments for page ${page}:`, error);
console.timeEnd(`Fetching page: ${page}`);
throw error; //Rethrow
}
}
}

View File

@ -1,46 +0,0 @@
import { Comment } from '../models/Comment';
import { DatabaseService } from '../../db/services/Database';
import { CommentFetcher } from './CommentFetcher';
/**
* CommentProcessor handles the retrieval and processing of comments from the r/Drama API.
* It manages API requests through CommentFetcher, including rate limiting and retries, and coordinates
* with DatabaseService for checking the existence and persisting new comments.
*/
export class CommentProcessor {
/**
* Fetches comments from the r/Drama API across multiple pages, up to the specified maximum.
* Iterates through pages starting from the first page until the maximum page limit is reached
* or there are no more comments to fetch. Each page's fetch operation is timed for performance analysis.
*
* @returns {Promise<Comment[]>} 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.
*/
static async processComments(maxPages: number = 10): Promise<Comment[]> {
let comments: Comment[] = [];
let stopFetching = false;
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
for (const comment of newComments) {
// Check if the comment was already processed in this batch
if (comments.some(c => c.id === comment.id)) continue;
const exists = await DatabaseService.commentExists(comment.id.toString());
if (exists) {
stopFetching = true;
break; // Stop processing this batch of comments
}
await DatabaseService.insertComment(comment)
comments.push(comment);
}
if (newComments.length === 0) break; // No more comments to fetch
}
return comments;
}
}

View File

@ -20,7 +20,7 @@ class SessionManager {
datastore: "ioredis",
clearDatastore: false,
clientOptions: {
host: process.env.REDIS_HOST,
host: process.env.REDIS_HOST!,
port: Number(process.env.REDIS_PORT),
password: process.env.REDIS_PASSWORD || undefined, // Use undefined if no password is set
enableOfflineQueue: true

View File

@ -1,18 +1,33 @@
import axios, { AxiosInstance, AxiosError } from 'axios';
import axios, { AxiosInstance, AxiosError, AxiosResponse, AxiosRequestConfig } from 'axios';
const qs = require('qs');
import dotenv from 'dotenv';
import axiosRetry from 'axios-retry';
import axiosThrottle from 'axios-request-throttle';
import Bottleneck from 'bottleneck';
import { DatabaseService } from '../../db/services/Database';
dotenv.config();
class RedditSessionManager {
private static instance: RedditSessionManager;
public axiosInstance: AxiosInstance;
public readonly axiosInstance: AxiosInstance;
private limiter: Bottleneck;
private constructor() {
axiosThrottle.use(axios, { requestsPerSecond: 1 }); // Throttle setup
// Initialize the Bottleneck limiter
this.limiter = new Bottleneck({
id: "reddit-limiter",
datastore: "ioredis",
clearDatastore: false,
clientOptions: {
host: process.env.REDIS_HOST,
port: Number(process.env.REDIS_PORT),
password: process.env.REDIS_PASSWORD || undefined, // Use undefined if no password is set
enableOfflineQueue: true
},
maxConcurrent: 1, // Maximum number of concurrent requests
minTime: 1000 // Minimum time between dispatches of requests in milliseconds
});
this.axiosInstance = axios.create({
baseURL: 'https://oauth.reddit.com/', // Base URL for OAuth2 Reddit API
@ -20,6 +35,10 @@ class RedditSessionManager {
'User-Agent': 'CrossTalk PM/0.1 by Whitneywisconson'
}
});
// Wrap axios requests with the limiter
this.wrapAxiosInstance(this.axiosInstance);
axiosRetry(this.axiosInstance, {
retries: 3,
retryDelay: this.retryDelayStrategy,
@ -35,6 +54,10 @@ class RedditSessionManager {
return RedditSessionManager.instance;
}
public async shutdown(): Promise<void> {
await this.limiter.disconnect();
}
private async initializeAuthentication() {
// Check the database for an existing token
const currentToken = await DatabaseService.getCurrentOAuthToken(this.axiosInstance.defaults.baseURL as string);
@ -103,6 +126,32 @@ class RedditSessionManager {
const status = error.response?.status ?? 0;
return status === 429 || status >= 400;
}
private wrapAxiosInstance(instance: AxiosInstance): void {
// Wrap the get method
const originalGet = instance.get;
instance.get = <T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R> => {
return this.limiter.schedule(() => originalGet.apply(instance, [url, config])) as Promise<R>;
};
// Wrap the post method
const originalPost = instance.post;
instance.post = <T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R> => {
return this.limiter.schedule(() => originalPost.apply(instance, [url, data, config])) as Promise<R>;
};
// Wrap the put method
const originalPut = instance.put;
instance.put = <T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R> => {
return this.limiter.schedule(() => originalPut.apply(instance, [url, data, config])) as Promise<R>;
};
// Wrap the delete method
const originalDelete = instance.delete;
instance.delete = <T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R> => {
return this.limiter.schedule(() => originalDelete.apply(instance, [url, config])) as Promise<R>;
};
}
}
export default RedditSessionManager;

View File

@ -0,0 +1,87 @@
import Redis, { RedisOptions } from 'ioredis';
import dotenv from 'dotenv';
dotenv.config();
interface RedisConfig extends RedisOptions {
host: string;
port: number;
password?: string;
}
class RedisSessionManager {
private static instance: RedisSessionManager;
public readonly client: Redis;
private subscriber: Redis | null = null;
private constructor() {
const redisConfig: RedisConfig = {
host: process.env.REDIS_HOST!,
port: Number(process.env.REDIS_PORT),
password: process.env.REDIS_PASSWORD || undefined,
showFriendlyErrorStack: true,
};
this.client = new Redis(redisConfig);
}
public static getInstance(): RedisSessionManager {
if (!RedisSessionManager.instance) {
RedisSessionManager.instance = new RedisSessionManager();
}
return RedisSessionManager.instance;
}
async subscribe(channel: string, messageHandler: (channel: string, message: string) => void): Promise<void> {
if (!this.subscriber) {
this.subscriber = new Redis({
host: process.env.REDIS_HOST!,
port: Number(process.env.REDIS_PORT),
password: process.env.REDIS_PASSWORD || undefined,
showFriendlyErrorStack: true,
});
}
await this.subscriber.subscribe(channel, (err, count) => {
if (err) {
console.error(`Failed to subscribe: ${err.message}`);
return;
}
console.log(`Subscribed to ${count} channel(s). Currently subscribed to ${channel}`);
});
this.subscriber.on('message', messageHandler);
}
async unsubscribe(channel: string): Promise<void> {
if (this.subscriber) {
await this.subscriber.unsubscribe(channel);
console.log(`Unsubscribed from ${channel}`);
}
}
async storeObject(keyPrefix: string, objectId: string, data: Record<string, any>, expirationSec: number = 3600): Promise<void> {
const key = `${keyPrefix}:${objectId}`;
const dataToStore = JSON.stringify(data);
await this.client.set(key, dataToStore, 'EX', expirationSec);
}
async retrieveObject(keyPrefix: string, objectId: string): Promise<Record<string, any> | null> {
const key = `${keyPrefix}:${objectId}`;
const data = await this.client.get(key);
return data ? JSON.parse(data) : null;
}
async publishObject(channel: string, data: Record<string, any>): Promise<number> {
const dataToPublish = JSON.stringify(data);
return this.client.publish(channel, dataToPublish);
}
async shutdown(): Promise<void> {
if (this.subscriber) {
await this.subscriber.quit();
}
await this.client.quit();
}
}
export default RedisSessionManager;

View File

@ -1,88 +1,48 @@
import fs from 'fs';
import path from 'path';
/**
* Manages the retrieval and formatting of messages stored in text files.
* This class provides functionality to load messages for rDrama and Reddit,
* select a random message, and replace placeholders within that message
* with specified values.
*/
// Define an enum for message file names
export enum MessageFileName {
RdramaPreviousMessage = 'rdrama_PreviousMessage.txt',
RdramaShouldntNotify = 'rdrama_ShouldntNotify.txt',
RedditMessages = 'reddit_messages.txt',
RdramaMessages = 'rdrama_messages.txt',
}
export class MessageService {
/**
* Loads rDrama messages from a text file, splitting by a specific delimiter.
* Loads messages from a specified text file based on the enum, splitting by a specific delimiter.
* Each message is separated by '---END---' in the text file.
*
* @example
* const rdramaMessages = MessageService.loadRdramaMessages();
*
* @returns {string[] | undefined} An array of rDrama messages, or undefined if there was an error loading the messages.
* @param {MessageFileName} fileName - The enum value representing the file containing the messages.
* @returns {string[] | undefined} An array of messages, or undefined if there was an error loading the messages.
*/
private static loadRdramaMessages(): string[] | undefined {
private static loadMessages(fileName: MessageFileName): string[] | undefined {
try {
const rdramaMessagesPath = path.join(__dirname, '..', 'messages', 'rdrama_messages.txt');
return fs.readFileSync(rdramaMessagesPath, 'utf-8').split('---END---').filter(line => line.trim());
const messagesPath = path.join(__dirname, '..', 'messages', fileName);
return fs.readFileSync(messagesPath, 'utf-8').split('---END---').filter(line => line.trim());
} catch (error) {
console.error('Failed to load rDrama messages:', error);
console.error(`Failed to load messages from ${fileName}:`, error);
}
}
/**
* Loads Reddit messages from a text file, splitting by a specific delimiter.
* Each message is separated by '---END---' in the text file.
*
* @example
* const redditMessages = MessageService.loadRedditMessages();
*
* @returns {string[] | undefined} An array of Reddit messages, or undefined if there was an error loading the messages.
*/
private static loadRedditMessages(): string[] | undefined {
try {
const redditMessagesPath = path.join(__dirname, '..', 'messages', 'reddit_messages.txt');
return fs.readFileSync(redditMessagesPath, 'utf-8').split('---END---').filter(line => line.trim());
} catch (error) {
console.error('Failed to load Reddit messages:', error);
}
}
/**
* Selects a random Reddit message from the loaded messages and replaces placeholders within it.
*
* @example
* const message = MessageService.getRandomRedditMessage({ username: 'exampleUser' });
* Selects a random message from the loaded messages and replaces placeholders within it.
*
* @param {MessageFileName} fileName - The enum value representing the file containing the messages to load.
* @param {Object} placeholders - A mapping of placeholder names to their replacement values.
* @returns {string | undefined} A formatted Reddit message with placeholders replaced, or undefined if messages couldn't be loaded.
* @returns {string | undefined} A formatted message with placeholders replaced, or undefined if messages couldn't be loaded.
*/
public static getRandomRedditMessage(placeholders: { [key: string]: string }): string | undefined {
const redditMessages = this.loadRedditMessages()
if (!redditMessages) return
const message = redditMessages[Math.floor(Math.random() * redditMessages.length)];
return this.replacePlaceholders(message, placeholders);
}
/**
* Selects a random rDrama message from the loaded messages and replaces placeholders within it.
*
* @example
* const message = MessageService.getRandomRdramaMessage({ username: 'exampleUser' });
*
* @param {Object} placeholders - A mapping of placeholder names to their replacement values.
* @returns {string | undefined} A formatted rDrama message with placeholders replaced, or undefined if messages couldn't be loaded.
*/
public static getRandomRdramaMessage(placeholders: { [key: string]: string }): string | undefined {
const rdramaMessages = this.loadRdramaMessages()
if (!rdramaMessages) return
const message = rdramaMessages[Math.floor(Math.random() * rdramaMessages.length)];
public static getRandomMessage(fileName: MessageFileName, placeholders: { [key: string]: string }): string | undefined {
const messages = this.loadMessages(fileName);
if (!messages) return undefined;
const message = messages[Math.floor(Math.random() * messages.length)];
return this.replacePlaceholders(message, placeholders);
}
/**
* Replaces placeholders in a message with values from a provided mapping.
*
* @example
* const formattedMessage = MessageService.replacePlaceholders('Hello, {username}!', { username: 'exampleUser' });
*
* @param {string} message - The message containing placeholders.
* @param {Object} placeholders - A mapping of placeholder names to their replacement values.
* @returns {string} The message with placeholders replaced by actual values.
@ -93,4 +53,4 @@ export class MessageService {
return acc.replace(regex, placeholders[key]);
}, message);
}
}
}

View File

@ -1,7 +1,6 @@
import { CommentProcessor } from "../rdrama/services/CommentProcessor";
import { CommentParser } from "../rdrama/services/CommentParser";
import { CommentPoster } from "../rdrama/services/CommentPoster";
import { MessageService } from "../utils/MessageService";
import { MessageFileName, MessageService } from "../utils/MessageService";
import { DatabaseService } from "../db/services/Database";
import { RedditService } from "../reddit/services/Reddit";
import { shouldNotifyUser } from "../utils/ShouldNotify";
@ -12,30 +11,20 @@ class WorkflowOrchestrator {
/**
* Executes the defined workflow for processing comments.
*/
async executeWorkflow() {
async executeWorkflow(comment: Comment) {
try {
const comments = await this.fetchAndLogComments();
for (const comment of comments) {
await this.processComment(comment);
const canSend = await DatabaseService.canSendNotification();
const coolDownHours = process.env.NOTIFICATION_COOLDOWN_HOURS
if (!canSend) {
console.log(`Last Message Sent less than ${coolDownHours ? coolDownHours : 4} hours ago. Set NOTIFICATION_COOLDOWN_HOURS to change this`)
return;
}
console.log('Workflow executed successfully.');
await this.processComment(comment);
} catch (error) {
console.error('An error occurred during workflow execution:', error);
}
}
/**
* Fetches comments and logs the count.
* @returns {Promise<Array>} The fetched comments.
*/
async fetchAndLogComments(): Promise<Comment[]> {
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.
@ -45,47 +34,45 @@ class WorkflowOrchestrator {
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);
await this.postCommentAndNotify(comment, redditUsers[0]);
}
/**
* 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.
async postCommentAndNotify(comment: Comment, redditUser: string) {
const placeholdersRdrama = { author_name: comment.author_name };
const userMentionExists = await DatabaseService.userMentionExists(redditUser);
if (userMentionExists) {
const commentPreviouslyMessaged = MessageService.getRandomMessage(MessageFileName.RdramaPreviousMessage, placeholdersRdrama);
if (!commentPreviouslyMessaged) throw new Error('No comments for previous Message found');
const postedComment = await CommentPoster.postComment(`c_${comment.id}`, `${commentPreviouslyMessaged}`);
console.log(`Sent Comment to`, JSON.stringify(postedComment, null, 4));
return;
}
const resultshouldNotifyUser = await shouldNotifyUser(redditUser);
if (!resultshouldNotifyUser) {
const commentShouldntNotify = MessageService.getRandomMessage(MessageFileName.RdramaShouldntNotify, placeholdersRdrama);
if (!commentShouldntNotify) throw new Error('No comments for Shouldnt Notify found');
const postedComment = await CommentPoster.postComment(`c_${comment.id}`, `${commentShouldntNotify}`);
console.log(`Sent Comment to`, JSON.stringify(postedComment, null, 4));
return;
}
const commentResponseRdrama = MessageService.getRandomMessage(MessageFileName.RdramaMessages, placeholdersRdrama);
if (!commentResponseRdrama) throw new Error('No comments for Rdrama found');
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);
const redditMessage = MessageService.getRandomMessage(MessageFileName.RedditMessages, placeholdersReddit);
if (!redditMessage) throw new Error('No comments for Reddit found');
await DatabaseService.insertUserMention({