272 lines
12 KiB
TypeScript
272 lines
12 KiB
TypeScript
import { Database } from 'sqlite';
|
|
import { Comment } from '../../rdrama/models/Comment';
|
|
import { DatabaseInitializer } from '../initializeDatabase';
|
|
|
|
/**
|
|
* Service for interacting with the SQLite database for operations related to comments and user mentions.
|
|
*/
|
|
export class DatabaseService {
|
|
/**
|
|
* Retrieves the singleton instance of the database.
|
|
* This static method ensures that a single database instance is used throughout the application,
|
|
* following the singleton pattern for managing database connections.
|
|
*
|
|
* @example
|
|
* const db = await DatabaseService.getDatabase();
|
|
*
|
|
* @returns {Promise<Database>} A promise that resolves to the initialized database instance.
|
|
* @throws {Error} Will throw an error if the database cannot be initialized.
|
|
*/
|
|
private static async getDatabase(): Promise<Database> {
|
|
const databaseInitializer = DatabaseInitializer.getInstance();
|
|
const db = await databaseInitializer.getDbInstance()
|
|
if (!db) {
|
|
throw new Error('Failed to initialize the database.');
|
|
}
|
|
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.
|
|
*
|
|
* @example
|
|
* await DatabaseService.insertUserMention({
|
|
* rdrama_comment_id: 456,
|
|
* username: 'mentionedUser',
|
|
* message: 'You were mentioned in a comment.'
|
|
* });
|
|
*
|
|
* @param {Object} mention - The user mention object to insert.
|
|
* @param {number} mention.rdrama_comment_id - The ID of the comment from the r/Drama platform.
|
|
* @param {string} mention.username - The mentioned Reddit username.
|
|
* @param {string} [mention.message] - The content of the message sent to the mentioned user.
|
|
* @throws {Error} Will throw an error if the insert operation fails.
|
|
*/
|
|
public static async insertUserMention(mention: { rdrama_comment_id: number; username: string; message?: string }): Promise<void> {
|
|
const db = await DatabaseService.getDatabase()
|
|
const sql = `INSERT INTO user_mentions (rdrama_comment_id, username, message) VALUES (?, ?, ?)`;
|
|
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.
|
|
*
|
|
* @example
|
|
* const mentioned = await DatabaseService.userMentionExists('exampleUser');
|
|
* console.log(mentioned ? 'User has been mentioned.' : 'User has not been mentioned.');
|
|
*
|
|
* @param {string} username - The username to search for.
|
|
* @returns {Promise<boolean>} A boolean indicating whether the username has been mentioned.
|
|
* @throws {Error} Will throw an error if the query operation fails.
|
|
*/
|
|
public static async userMentionExists(username: string): Promise<boolean> {
|
|
const db = await DatabaseService.getDatabase()
|
|
const sql = `SELECT 1 FROM user_mentions WHERE username = ?`;
|
|
const result = await db.get(sql, [username]);
|
|
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.
|
|
*
|
|
* @example
|
|
* await DatabaseService.upsertOAuthToken('https://oauth.reddit.com', {
|
|
* access_token: 'abc123',
|
|
* token_type: 'bearer',
|
|
* expires_in: 3600,
|
|
* scope: 'read'
|
|
* });
|
|
*
|
|
* @param {string} token_identifier - A unique identifier for the token, typically the service's base URL.
|
|
* @param {Object} tokenData - The OAuth token data including access_token, token_type, expires_in, and scope.
|
|
* @throws {Error} Will throw an error if the upsert operation fails.
|
|
*/
|
|
public static async upsertOAuthToken(token_identifier: string, tokenData: any) {
|
|
const db = await DatabaseService.getDatabase()
|
|
const { access_token, token_type, expires_in, scope } = tokenData;
|
|
const expiryTimestamp = Math.floor(Date.now() / 1000) + expires_in;
|
|
console.log('token_identifier', token_identifier)
|
|
console.log('access_token', `${access_token.substring(0, 5)}XXXXX`)
|
|
console.log('token_type', token_type)
|
|
console.log('expires_in', expires_in)
|
|
console.log('scope', scope)
|
|
|
|
await db.run(`
|
|
INSERT INTO oauth_tokens (token_identifier, access_token, token_type, expires_in, expiry_timestamp, scope)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(token_identifier) DO UPDATE SET
|
|
access_token = excluded.access_token,
|
|
token_type = excluded.token_type,
|
|
expires_in = excluded.expires_in,
|
|
expiry_timestamp = excluded.expiry_timestamp,
|
|
scope = excluded.scope
|
|
`, [token_identifier, access_token, token_type, expires_in, expiryTimestamp, scope]);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the current, unexpired OAuth token for a specific service.
|
|
*
|
|
* @example
|
|
* const token = await DatabaseService.getCurrentOAuthToken('https://oauth.reddit.com');
|
|
* console.log(token ? `Current token: ${token.access_token}` : 'No valid token found.');
|
|
*
|
|
* @param {string} token_identifier - The unique identifier for the token, typically the service's base URL.
|
|
* @returns {Promise<Object|null>} The current OAuth token data or null if expired or not found.
|
|
* @throws {Error} Will throw an error if the query operation fails.
|
|
*/
|
|
public static async getCurrentOAuthToken(token_identifier: string) {
|
|
const db = await DatabaseService.getDatabase()
|
|
const tokenRow = await db.get(`
|
|
SELECT access_token, token_type, scope, expiry_timestamp FROM oauth_tokens
|
|
WHERE token_identifier = ?
|
|
`, token_identifier);
|
|
|
|
return tokenRow || null;
|
|
}
|
|
|
|
/**
|
|
* Checks if the cooldown period has passed since the last notification was sent, allowing for a new notification to be sent.
|
|
*
|
|
* @example
|
|
* const canSend = await DatabaseService.canSendNotification();
|
|
* console.log(canSend ? 'Can send a new notification.' : 'Still in cooldown period.');
|
|
*
|
|
* @returns {Promise<boolean>} True if the cooldown period has passed, allowing new notifications to be sent.
|
|
* @throws {Error} Will throw an error if the check operation fails.
|
|
*/
|
|
public static async canSendNotification(): Promise<boolean> {
|
|
const db = await DatabaseService.getDatabase()
|
|
const cooldownHours = process.env.NOTIFICATION_COOLDOWN_HOURS || 4;
|
|
const sql = `
|
|
SELECT MAX(sent_time) as last_notification_time
|
|
FROM user_mentions
|
|
`;
|
|
const result = await 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 = new Date(new Date().toISOString().slice(0, 19).replace('T', ' ')).getTime();
|
|
const timeElapsed = currentTime - lastNotificationTime;
|
|
console.log('timeElapsed', timeElapsed)
|
|
const cooldownPeriod = +cooldownHours * 60 * 60 * 1000; // Convert hours to milliseconds
|
|
console.log('cooldownPeriod', cooldownPeriod)
|
|
|
|
return timeElapsed >= cooldownPeriod;
|
|
}
|
|
|
|
}
|