notification logic

master
sloppyjosh 2024-03-05 01:25:05 -05:00
parent faa74287bb
commit 35956bb7b1
8 changed files with 228 additions and 55 deletions

View File

@ -48,7 +48,7 @@ export class DatabaseService {
* *
* @param {Object} mention - The user mention object to insert, containing rdrama_comment_id, username, and optionally message. * @param {Object} mention - The user mention object to insert, containing rdrama_comment_id, username, and optionally message.
*/ */
public async insertUserMention(mention: { rdrama_comment_id: string; username: string; message?: string }): Promise<void> { public async insertUserMention(mention: { rdrama_comment_id: number; username: string; message?: string }): Promise<void> {
const sql = `INSERT INTO user_mentions (rdrama_comment_id, username, message) VALUES (?, ?, ?)`; const sql = `INSERT INTO user_mentions (rdrama_comment_id, username, message) VALUES (?, ?, ?)`;
await this.db.run(sql, [mention.rdrama_comment_id, mention.username, mention.message]); await this.db.run(sql, [mention.rdrama_comment_id, mention.username, mention.message]);
} }
@ -164,4 +164,31 @@ export class DatabaseService {
return tokenRow || null; return tokenRow || null;
} }
/**
* Checks if the cooldown period has passed since the last notification was sent to any user.
*
* @returns {Promise<boolean>} True if the cooldown has passed, allowing new notifications to be sent.
*/
public async canSendNotification(): Promise<boolean> {
const cooldownHours = parseInt(process.env.NOTIFICATION_COOLDOWN_HOURS || '4', 10);
const sql = `
SELECT MAX(sent_time) as last_notification_time
FROM user_mentions
`;
const result = await this.db.get(sql);
if (!result || !result.last_notification_time) {
// No notifications have been sent yet, or unable to retrieve the last sent time.
return true;
}
const lastNotificationTime = new Date(result.last_notification_time).getTime();
const currentTime = Date.now();
const timeElapsed = currentTime - lastNotificationTime;
const cooldownPeriod = cooldownHours * 60 * 60 * 1000; // Convert hours to milliseconds
return timeElapsed >= cooldownPeriod;
}
} }

View File

@ -14,6 +14,20 @@ import { CommentPoster } from './rdrama/services/CommentPoster';
// Import other necessary services or configurations // Import other necessary services or configurations
async function startApplication() { async function startApplication() {
console.log('Database Start')
const databaseInitializer = DatabaseInitializer.getInstance();
const db = await databaseInitializer.getDbInstance()
if (!db) {
throw new Error('Failed to initialize the database.');
}
const databaseService = new DatabaseService(db)
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`)
}
console.log('RDrama Session Start')
// Initialize SessionManager or other global configurations // Initialize SessionManager or other global configurations
const rDramaSessionManager = rDramaSession.getInstance(); const rDramaSessionManager = rDramaSession.getInstance();
if (!process.env.RDRAMA_API_KEY) { if (!process.env.RDRAMA_API_KEY) {
@ -21,16 +35,11 @@ async function startApplication() {
} }
rDramaSessionManager.setAuthorizationToken(process.env.RDRAMA_API_KEY); rDramaSessionManager.setAuthorizationToken(process.env.RDRAMA_API_KEY);
const databaseInitializer = DatabaseInitializer.getInstance();
const db = await databaseInitializer.getDbInstance()
if (!db) {
throw new Error('Failed to initialize the database.');
}
const databaseService = new DatabaseService(db)
const databaseMaintenance = new DatabaseMaintenanceService(databaseService)
console.log('Database Maintenance Start') console.log('Database Maintenance Start')
const databaseMaintenance = new DatabaseMaintenanceService(databaseService)
await databaseMaintenance.runMaintenanceTasks() await databaseMaintenance.runMaintenanceTasks()
console.log('Reddit Session Start')
await redditSession.getInstance(databaseService) await redditSession.getInstance(databaseService)
// Initialize services with any required dependencies // Initialize services with any required dependencies
@ -45,6 +54,7 @@ async function startApplication() {
commentParser, commentParser,
commentPoster, commentPoster,
messageService, messageService,
databaseService,
); );
await workflowOrchestrator.executeWorkflow(); await workflowOrchestrator.executeWorkflow();
} }

View File

@ -0,0 +1,44 @@
export type Subreddit = {
default_set?: boolean;
user_is_contributor?: boolean;
banner_img?: string;
allowed_media_in_comments?: any[];
user_is_banned?: boolean;
free_form_reports?: boolean;
community_icon?: string | null;
show_media?: boolean;
icon_color?: string;
user_is_muted?: boolean | null;
display_name?: string;
header_img?: string | null;
title?: string;
coins?: number;
previous_names?: any[];
over_18?: boolean;
icon_size?: number[] | null;
primary_color?: string;
icon_img?: string;
description?: string;
submit_link_label?: string;
header_size?: number[] | null;
restrict_posting?: boolean;
restrict_commenting?: boolean;
subscribers?: number;
submit_text_label?: string;
is_default_icon?: boolean;
link_flair_position?: string;
display_name_prefixed?: string;
key_color?: string;
name?: string;
is_default_banner?: boolean;
url?: string;
quarantine?: boolean;
banner_size?: number[] | null;
user_is_moderator?: boolean;
accept_followers?: boolean;
public_description?: string;
link_flair_enabled?: boolean;
disable_contributor_requests?: boolean;
subreddit_type?: string;
user_is_subscriber?: boolean;
};

View File

@ -0,0 +1,71 @@
import { Subreddit } from "./Subreddit";
export type RedditUser = {
kind: string;
data: {
is_employee?: boolean;
has_visited_new_profile?: boolean;
is_friend: boolean;
pref_no_profanity?: boolean;
has_external_account?: boolean;
pref_geopopular?: string;
pref_show_trending?: boolean;
subreddit: Subreddit
pref_show_presence?: boolean;
snoovatar_img?: string;
snoovatar_size?: number[] | null;
gold_expiration?: null;
has_gold_subscription?: boolean;
is_sponsor?: boolean;
num_friends?: number;
features?: any;
can_edit_name?: boolean;
is_blocked?: boolean;
verified?: boolean;
new_modmail_exists?: null;
pref_autoplay?: boolean;
coins?: number;
has_paypal_subscription?: boolean;
has_subscribed_to_premium?: boolean;
id: string;
can_create_subreddit?: boolean;
over_18?: boolean;
is_gold?: boolean;
is_mod?: boolean;
awarder_karma?: number;
suspension_expiration_utc?: null;
has_stripe_subscription?: boolean;
is_suspended?: boolean;
pref_video_autoplay?: boolean;
in_chat?: boolean;
has_android_subscription?: boolean;
in_redesign_beta?: boolean;
icon_img: string;
has_mod_mail?: boolean;
pref_nightmode?: boolean;
awardee_karma?: number;
hide_from_robots?: boolean;
password_set?: boolean;
modhash?: null;
link_karma: number;
force_password_reset?: boolean;
total_karma: number;
inbox_count?: number;
pref_top_karma_subreddits?: boolean;
has_mail?: boolean;
pref_show_snoovatar?: boolean;
name: string;
pref_clickgadget?: number;
created: number;
has_verified_email: boolean;
gold_creddits?: number;
created_utc: number;
has_ios_subscription?: boolean;
pref_show_twitter?: boolean;
in_beta?: boolean;
comment_karma: number;
accept_followers: boolean;
has_subscribed: boolean;
accept_pms?: boolean;
}
};

View File

@ -1,5 +1,6 @@
import axios, { AxiosError } from 'axios'; import axios, { AxiosError } from 'axios';
import SessionManager from '../session/SessionManager'; import SessionManager from '../session/SessionManager';
import { RedditUser } from '../model/user';
export class RedditService { export class RedditService {
private sessionManager: SessionManager; private sessionManager: SessionManager;
@ -8,7 +9,7 @@ export class RedditService {
this.sessionManager = sessionManager; this.sessionManager = sessionManager;
} }
async getUserInfo(username: string): Promise<any> { async getUserInfo(username: string): Promise<RedditUser> {
try { try {
const response = await this.sessionManager.axiosInstance.get(`/user/${username}/about`); const response = await this.sessionManager.axiosInstance.get(`/user/${username}/about`);
return response.data; return response.data;
@ -20,12 +21,13 @@ export class RedditService {
async sendMessage(username: string, subject: string, message: string): Promise<void> { async sendMessage(username: string, subject: string, message: string): Promise<void> {
try { try {
await this.sessionManager.axiosInstance.post('/api/compose', { console.log(`await this.sessionManager.axiosInstance.post('/api/compose', {\n\tapi_type: 'json',\n\tto: ${username},\n\tsubject: ${subject},\n\ttext: ${message},\n});`)
api_type: 'json', //await this.sessionManager.axiosInstance.post('/api/compose', {
to: username, // api_type: 'json',
subject: subject, // to: username,
text: message, // subject: subject,
}); // text: message,
//});
console.log(`Message sent to ${username}`); console.log(`Message sent to ${username}`);
} catch (error) { } catch (error) {
console.error('Error sending message:', error); console.error('Error sending message:', error);

View File

@ -17,7 +17,7 @@ class RedditSessionManager {
axiosThrottle.use(axios, { requestsPerSecond: 1 }); // Throttle setup axiosThrottle.use(axios, { requestsPerSecond: 1 }); // Throttle setup
this.axiosInstance = axios.create({ this.axiosInstance = axios.create({
baseURL: 'https://oauth.reddit.com/api/v1/', // Base URL for OAuth2 Reddit API baseURL: 'https://oauth.reddit.com/', // Base URL for OAuth2 Reddit API
headers: { headers: {
'User-Agent': 'CrossTalk PM/0.1 by Whitneywisconson' 'User-Agent': 'CrossTalk PM/0.1 by Whitneywisconson'
} }

View File

@ -0,0 +1,28 @@
import dotenv from 'dotenv';
import { RedditService } from '../reddit/services/reddit';
import { DatabaseService } from '../db/services/Database';
// Load environment variables from .env file
dotenv.config();
export async function shouldNotifyUser(username: string, redditService: RedditService, databaseService: DatabaseService): Promise<boolean> {
const userInfo = await redditService.getUserInfo(username);
if (!userInfo) return false;
const { is_mod, is_employee, accept_pms, total_karma } = userInfo.data;
const excludeMods = process.env.EXCLUDE_MODS !== 'false'; // Defaults to true unless explicitly set to 'false'
const excludeEmployees = process.env.EXCLUDE_EMPLOYEES !== 'false'; // Defaults to true unless explicitly set to 'false'
const notifyAcceptPms = accept_pms !== false; // Notify if accept_pms is true or undefined
const karmaThreshold = parseInt(process.env.KARMA_THRESHOLD || '100000', 10);
const hasBeenNotifiedBefore = await databaseService.userMentionExists(username);
const meetsCriteria =
(!excludeMods || !is_mod) && // Notify unless we're excluding mods and the user is a mod
(!excludeEmployees || !is_employee) && // Notify unless we're excluding employees and the user is an employee
notifyAcceptPms &&
total_karma < karmaThreshold &&
!hasBeenNotifiedBefore;
return meetsCriteria;
}

View File

@ -2,6 +2,10 @@ import { CommentProcessor } from "../rdrama/services/CommentProcessor";
import { CommentParser } from "../rdrama/services/CommentParser"; import { CommentParser } from "../rdrama/services/CommentParser";
import { CommentPoster } from "../rdrama/services/CommentPoster"; import { CommentPoster } from "../rdrama/services/CommentPoster";
import { MessageService } from "../utils/MessageService"; 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";
class WorkflowOrchestrator { class WorkflowOrchestrator {
constructor( constructor(
@ -9,7 +13,7 @@ class WorkflowOrchestrator {
private commentParser: CommentParser, private commentParser: CommentParser,
private commentPoster: CommentPoster, private commentPoster: CommentPoster,
private messageService: MessageService, private messageService: MessageService,
//private redditNotifier: RedditNotifier // Handles notifications to Reddit users private databaseService: DatabaseService,
) { } ) { }
/** /**
@ -30,52 +34,39 @@ class WorkflowOrchestrator {
const redditUsers = this.commentParser.extractUsernames(comment) const redditUsers = this.commentParser.extractUsernames(comment)
if (redditUsers.length === 0) continue if (redditUsers.length === 0) continue
console.log('found:', redditUsers) console.log('found:', redditUsers)
const placeholders = { const placeholdersRdrama = {
author_name: comment.author_name, author_name: comment.author_name,
}; };
const commentResponse = this.messageService.getRandomRdramaMessage(placeholders) for (const redditUser of redditUsers) {
const postedComment = await this.commentPoster.postComment(`c_${comment.id}`, `##### TEST MESSAGE NO REDDITOR PINGED (YET...)\n${commentResponse}`) const userMentionExists = await this.databaseService.userMentionExists(redditUser)
//const postedComment = await this.commentPoster.postComment(`c_${comment.id}`, ${commentResponse}`) //TODO make this live if (userMentionExists) continue
console.log(`Sent Comment to`, JSON.stringify(postedComment, null, 4)) const commentResponseRdrama = this.messageService.getRandomRdramaMessage(placeholdersRdrama)
//FOR now, only reply to one user and we would check against DB with a random interval, we should only message between 2-3 people daily? 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(this.databaseService)
const redditService = new RedditService(redditSession)
const resultshouldNotifyUser = await shouldNotifyUser(redditUser, redditService, this.databaseService)
if (!resultshouldNotifyUser) continue
const placeholdersReddit = {
author_name: comment.author_name,
};
const redditMessage = this.messageService.getRandomRedditMessage(placeholdersReddit)
await this.databaseService.insertUserMention({
rdrama_comment_id: comment.id,
username: redditUser,
message: redditMessage,
})
await redditService.sendMessage(redditUser, 'Crosstalk PM Notification', redditMessage)
return;
}
} }
//// Query user information based on usernames
//const userInfo = await this.databaseService.queryUsersInfo(uniqueUsernames);
//console.log(`Queried information for ${userInfo.length} users`);
//
//// Filter users who should be notified
//const usersToNotify = userInfo.filter(user => this.shouldNotifyUser(user));
//console.log(`Identified ${usersToNotify.length} users to notify`);
//
//// Notify users
//for (const user of usersToNotify) {
// await this.redditNotifier.notifyUser(user);
// console.log(`Notified user: ${user.username}`);
//}
console.log('Workflow executed successfully.'); console.log('Workflow executed successfully.');
} catch (error) { } catch (error) {
console.error('An error occurred during workflow execution:', error); console.error('An error occurred during workflow execution:', error);
} }
} }
/**
* Determines whether a user should be notified based on certain criteria.
*
* @param user - The user information object.
* @returns A boolean indicating whether the user should be notified.
*/
//private shouldNotifyUser(user: UserInfo): boolean {
// // Placeholder for the actual logic to determine if a user should be notified.
// // This could involve checking the last notification time against the current time,
// // user preferences, or other criteria defined in the business logic.
//
// // Example logic (to be replaced with actual implementation):
// const lastNotifiedTime = new Date(user.lastNotified); // Assuming 'lastNotified' is a Date or string.
// const notificationThreshold = 24 * 60 * 60 * 1000; // 24 hours in milliseconds.
// return (Date.now() - lastNotifiedTime.getTime()) > notificationThreshold;
//}
} }
export default WorkflowOrchestrator; export default WorkflowOrchestrator;