notification logic
parent
faa74287bb
commit
35956bb7b1
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
24
src/index.ts
24
src/index.ts
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
||||||
|
const commentResponseRdrama = this.messageService.getRandomRdramaMessage(placeholdersRdrama)
|
||||||
|
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))
|
console.log(`Sent Comment to`, JSON.stringify(postedComment, null, 4))
|
||||||
//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 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;
|
Loading…
Reference in New Issue