Initial Commit

master
sloppyjosh 2024-02-22 01:13:41 -05:00
parent 11abe9b79e
commit b50abbfb92
11 changed files with 3008 additions and 0 deletions

2732
package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

View File

@ -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();

View File

@ -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
);

View File

@ -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);

View File

@ -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;
};

View File

@ -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);
}
}

View File

@ -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;

View File

View File

View File

View File