Initial Commit
parent
11abe9b79e
commit
b50abbfb92
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,92 @@
|
||||||
|
import { Database, open } from 'sqlite';
|
||||||
|
import sqlite3 from 'sqlite3';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton class responsible for initializing and setting up the SQLite database.
|
||||||
|
* It ensures that only one instance of the database is created and utilized throughout the application.
|
||||||
|
*/
|
||||||
|
class DatabaseInitializer {
|
||||||
|
private static instance: DatabaseInitializer;
|
||||||
|
private db: Database | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DatabaseInitializer's constructor is private to prevent direct instantiation with the `new` operator
|
||||||
|
* and ensure the Singleton pattern is followed.
|
||||||
|
*/
|
||||||
|
private constructor() {
|
||||||
|
this.setupDatabase().catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the singleton instance of the DatabaseInitializer.
|
||||||
|
* @returns The singleton instance of the DatabaseInitializer.
|
||||||
|
*/
|
||||||
|
public static getInstance(): DatabaseInitializer {
|
||||||
|
if (!DatabaseInitializer.instance) {
|
||||||
|
DatabaseInitializer.instance = new DatabaseInitializer();
|
||||||
|
}
|
||||||
|
return DatabaseInitializer.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the SQLite database. If the database file does not exist, it will be created.
|
||||||
|
* @param dbPath The path to the SQLite database file.
|
||||||
|
* @returns A promise that resolves with the Database instance.
|
||||||
|
* @throws {Error} Throws an error if there's an issue opening the database.
|
||||||
|
*/
|
||||||
|
private async initializeDatabase(dbPath: string): Promise<Database> {
|
||||||
|
try {
|
||||||
|
const db = await open({
|
||||||
|
filename: dbPath,
|
||||||
|
driver: sqlite3.Database
|
||||||
|
});
|
||||||
|
console.log('Database initialized successfully.');
|
||||||
|
return db;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize the database:', error);
|
||||||
|
throw new Error('Failed to initialize the database.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes SQL files found within a specified folder. This function is designed to run migration and seed files.
|
||||||
|
* @param db The SQLite database instance.
|
||||||
|
* @param folderName The name of the folder containing the SQL files, relative to the class location.
|
||||||
|
* @throws {Error} Throws an error if there's an issue reading the directory or executing SQL files.
|
||||||
|
*/
|
||||||
|
private async runSqlFiles(db: Database, folderName: string): Promise<void> {
|
||||||
|
const folderPath = path.join(__dirname, '..', folderName); // Adjust for class location within src/db
|
||||||
|
const files = await fs.readdir(folderPath);
|
||||||
|
const sqlFiles = files.filter(file => file.endsWith('.sql'));
|
||||||
|
|
||||||
|
if (sqlFiles.length === 0) {
|
||||||
|
console.log(`No SQL files found in ${folderName}. Skipping.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of sqlFiles.sort()) {
|
||||||
|
const sql = await fs.readFile(path.join(folderPath, file), 'utf8');
|
||||||
|
await db.exec(sql);
|
||||||
|
console.log(`Executed ${file} in ${folderName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main method for setting up the database. It runs the migrations first, then seeds the database.
|
||||||
|
* @example
|
||||||
|
* DatabaseInitializer.getInstance();
|
||||||
|
*/
|
||||||
|
private async setupDatabase(): Promise<void> {
|
||||||
|
const dbPath = path.join(__dirname, 'appData.db');
|
||||||
|
this.db = await this.initializeDatabase(dbPath);
|
||||||
|
|
||||||
|
await this.runSqlFiles(this.db!, 'migrations');
|
||||||
|
await this.runSqlFiles(this.db!, 'seed');
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example of how to use the DatabaseInitializer
|
||||||
|
const dbInitializer = DatabaseInitializer.getInstance();
|
|
@ -0,0 +1,23 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS comments (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
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
|
||||||
|
);
|
|
@ -0,0 +1,28 @@
|
||||||
|
CREATE TABLE user_mentions (
|
||||||
|
-- Unique identifier for each record. Auto-incremented to ensure uniqueness.
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
|
||||||
|
-- The unique identifier of the comment from the r/Drama platform. Not unique in this table
|
||||||
|
-- because a single comment can mention multiple users.
|
||||||
|
rdrama_comment_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- The mentioned Reddit username in a standardized format (e.g., u/username). Lowercased
|
||||||
|
-- to ensure consistency and prevent duplicate entries due to case differences.
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- The content of the message sent to the mentioned user, if any. Allows tracking
|
||||||
|
-- of what communication has been made, useful for audit purposes or resending messages.
|
||||||
|
message TEXT,
|
||||||
|
|
||||||
|
-- Timestamp when the mention was processed and, if applicable, when a message was sent.
|
||||||
|
-- Defaults to the current timestamp at the time of record creation.
|
||||||
|
sent_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
-- Enforces uniqueness for each comment-username pair to prevent processing and notifying
|
||||||
|
-- the same user mention in the same comment more than once.
|
||||||
|
CONSTRAINT unique_comment_user UNIQUE (rdrama_comment_id, username)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Consider adding indexes based on query patterns for improved performance, such as:
|
||||||
|
-- CREATE INDEX idx_username ON user_mentions(username);
|
||||||
|
-- CREATE INDEX idx_rdrama_comment_id ON user_mentions(rdrama_comment_id);
|
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* Represents a comment from the r/Drama platform, containing details about the comment's author,
|
||||||
|
* content, status, and other metadata.
|
||||||
|
* @typedef {Object} Comment
|
||||||
|
* @property {number} author_id - The unique identifier for the author of the comment.
|
||||||
|
* @property {string} author_name - The display name of the author.
|
||||||
|
* @property {string} body - The raw text content of the comment.
|
||||||
|
* @property {string} body_html - The HTML-rendered version of the comment text.
|
||||||
|
* @property {number} created_utc - The UTC timestamp when the comment was created.
|
||||||
|
* @property {number} deleted_utc - The UTC timestamp when the comment was deleted, if applicable.
|
||||||
|
* @property {boolean} distinguished - Flag indicating if the comment is distinguished by the platform (e.g., admin posts).
|
||||||
|
* @property {number} downvotes - The number of downvotes the comment has received.
|
||||||
|
* @property {number} edited_utc - The UTC timestamp when the comment was last edited, if ever.
|
||||||
|
* @property {number} id - The unique identifier for the comment.
|
||||||
|
* @property {boolean} is_banned - Flag indicating if the author was banned at the time of the comment.
|
||||||
|
* @property {boolean} is_bot - Flag indicating if the comment was made by a bot.
|
||||||
|
* @property {boolean} is_nsfw - Flag indicating if the comment is marked as NSFW (Not Safe For Work).
|
||||||
|
* @property {number} level - The nesting level of the comment in the conversation thread.
|
||||||
|
* @property {string} permalink - The permanent link to the comment.
|
||||||
|
* @property {string} pinned - The name of the user who pinned the comment, if any.
|
||||||
|
* @property {number} post_id - The unique identifier of the post to which the comment belongs.
|
||||||
|
* @property {number[]} replies - An array of comment IDs representing the direct replies to this comment.
|
||||||
|
* @property {Record<string, unknown>} reports - An object containing any reports made on the comment.
|
||||||
|
* @property {number} score - The total score of the comment (upvotes - downvotes).
|
||||||
|
* @property {number} upvotes - The number of upvotes the comment has received.
|
||||||
|
*/
|
||||||
|
export type Comment = {
|
||||||
|
author_id: number;
|
||||||
|
author_name: string;
|
||||||
|
body: string;
|
||||||
|
body_html: string;
|
||||||
|
created_utc: number;
|
||||||
|
deleted_utc: number;
|
||||||
|
distinguished: boolean;
|
||||||
|
downvotes: number;
|
||||||
|
edited_utc: number;
|
||||||
|
id: number;
|
||||||
|
is_banned: boolean;
|
||||||
|
is_bot: boolean;
|
||||||
|
is_nsfw: boolean;
|
||||||
|
level: number;
|
||||||
|
permalink: string;
|
||||||
|
pinned: string;
|
||||||
|
post_id: number;
|
||||||
|
replies: number[];
|
||||||
|
reports: Record<string, unknown>;
|
||||||
|
score: number;
|
||||||
|
upvotes: number;
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
// src/services/CommentService.ts
|
||||||
|
|
||||||
|
import { Comment } from '../models/Comment';
|
||||||
|
|
||||||
|
export class CommentService {
|
||||||
|
private regexPattern: RegExp = /(^|\s|\\r\\n|\\t|[".,;(){}\[\]!?@#])(\/?u\/[a-zA-Z0-9_]+)/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts Reddit usernames from the body of a single comment.
|
||||||
|
* @param comment A single Comment object to be processed.
|
||||||
|
* @returns An array of unique Reddit usernames found in the comment.
|
||||||
|
*/
|
||||||
|
public extractUsernames(comment: Comment): string[] {
|
||||||
|
const foundUsernames: Set<string> = new Set();
|
||||||
|
|
||||||
|
const matches = comment.body.match(this.regexPattern);
|
||||||
|
if (matches) {
|
||||||
|
matches.forEach(match => {
|
||||||
|
// Ensure the username is captured in a standardized format
|
||||||
|
const usernameMatch = match.trim().match(/\/?u\/([a-zA-Z0-9_]+)/);
|
||||||
|
if (usernameMatch) {
|
||||||
|
// Standardize to "u/username" format
|
||||||
|
const username = `u/${usernameMatch[1].toLowerCase()}`;
|
||||||
|
foundUsernames.add(username);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(foundUsernames);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import axios, {AxiosInstance, AxiosError} from 'axios';
|
||||||
|
import axiosRetry from 'axios-retry';
|
||||||
|
import axiosThrottle from 'axios-request-throttle';
|
||||||
|
|
||||||
|
class SessionManager {
|
||||||
|
private static instance: SessionManager;
|
||||||
|
public readonly axiosInstance: AxiosInstance;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
axiosThrottle.use(axios, { requestsPerSecond: 1 }); // Throttle setup
|
||||||
|
|
||||||
|
this.axiosInstance = axios.create({
|
||||||
|
baseURL: 'https://rdrama.net/',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
axiosRetry(this.axiosInstance, {
|
||||||
|
retries: 3,
|
||||||
|
retryDelay: this.retryDelayStrategy,
|
||||||
|
retryCondition: this.retryCondition,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): SessionManager {
|
||||||
|
if (!SessionManager.instance) {
|
||||||
|
SessionManager.instance = new SessionManager();
|
||||||
|
}
|
||||||
|
return SessionManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private retryDelayStrategy(retryCount: number, error: AxiosError): number {
|
||||||
|
const retryAfter = error.response?.headers['retry-after'];
|
||||||
|
if (retryAfter) {
|
||||||
|
console.log(`429 Retry After: ${retryAfter}`);
|
||||||
|
return +retryAfter * 1000;
|
||||||
|
}
|
||||||
|
return Math.pow(2, retryCount) * 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
private retryCondition(error: AxiosError): boolean {
|
||||||
|
const status = error.response?.status ?? 0;
|
||||||
|
return status === 429 || status >= 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setAuthorizationToken(token: string): void {
|
||||||
|
this.axiosInstance.defaults.headers.common['Authorization'] = `${token}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SessionManager;
|
Loading…
Reference in New Issue