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