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.
|
||||
*/
|
||||
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 (?, ?, ?)`;
|
||||
await this.db.run(sql, [mention.rdrama_comment_id, mention.username, mention.message]);
|
||||
}
|
||||
|
@ -164,4 +164,31 @@ export class DatabaseService {
|
|||
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
|
||||
|
||||
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
|
||||
const rDramaSessionManager = rDramaSession.getInstance();
|
||||
if (!process.env.RDRAMA_API_KEY) {
|
||||
|
@ -21,16 +35,11 @@ async function startApplication() {
|
|||
}
|
||||
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')
|
||||
const databaseMaintenance = new DatabaseMaintenanceService(databaseService)
|
||||
await databaseMaintenance.runMaintenanceTasks()
|
||||
|
||||
console.log('Reddit Session Start')
|
||||
await redditSession.getInstance(databaseService)
|
||||
|
||||
// Initialize services with any required dependencies
|
||||
|
@ -45,6 +54,7 @@ async function startApplication() {
|
|||
commentParser,
|
||||
commentPoster,
|
||||
messageService,
|
||||
databaseService,
|
||||
);
|
||||
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 SessionManager from '../session/SessionManager';
|
||||
import { RedditUser } from '../model/user';
|
||||
|
||||
export class RedditService {
|
||||
private sessionManager: SessionManager;
|
||||
|
@ -8,7 +9,7 @@ export class RedditService {
|
|||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
async getUserInfo(username: string): Promise<any> {
|
||||
async getUserInfo(username: string): Promise<RedditUser> {
|
||||
try {
|
||||
const response = await this.sessionManager.axiosInstance.get(`/user/${username}/about`);
|
||||
return response.data;
|
||||
|
@ -20,12 +21,13 @@ export class RedditService {
|
|||
|
||||
async sendMessage(username: string, subject: string, message: string): Promise<void> {
|
||||
try {
|
||||
await this.sessionManager.axiosInstance.post('/api/compose', {
|
||||
api_type: 'json',
|
||||
to: username,
|
||||
subject: subject,
|
||||
text: message,
|
||||
});
|
||||
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,
|
||||
//});
|
||||
console.log(`Message sent to ${username}`);
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
|
|
|
@ -17,7 +17,7 @@ class RedditSessionManager {
|
|||
axiosThrottle.use(axios, { requestsPerSecond: 1 }); // Throttle setup
|
||||
|
||||
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: {
|
||||
'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 { 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";
|
||||
|
||||
class WorkflowOrchestrator {
|
||||
constructor(
|
||||
|
@ -9,7 +13,7 @@ class WorkflowOrchestrator {
|
|||
private commentParser: CommentParser,
|
||||
private commentPoster: CommentPoster,
|
||||
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)
|
||||
if (redditUsers.length === 0) continue
|
||||
console.log('found:', redditUsers)
|
||||
const placeholders = {
|
||||
const placeholdersRdrama = {
|
||||
author_name: comment.author_name,
|
||||
};
|
||||
const commentResponse = this.messageService.getRandomRdramaMessage(placeholders)
|
||||
const postedComment = await this.commentPoster.postComment(`c_${comment.id}`, `##### TEST MESSAGE NO REDDITOR PINGED (YET...)\n${commentResponse}`)
|
||||
//const postedComment = await this.commentPoster.postComment(`c_${comment.id}`, ${commentResponse}`) //TODO make this live
|
||||
for (const redditUser of redditUsers) {
|
||||
const userMentionExists = await this.databaseService.userMentionExists(redditUser)
|
||||
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))
|
||||
//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.');
|
||||
} catch (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;
|
Loading…
Reference in New Issue