Initial commit — OFApp client + server
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
129
server/db.js
Normal file
129
server/db.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { mkdirSync, existsSync } from 'fs';
|
||||
import { dirname } from 'path';
|
||||
|
||||
const DB_PATH = process.env.DB_PATH || './data/db/ofapp.db';
|
||||
|
||||
const dir = dirname(DB_PATH);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const db = new Database(DB_PATH);
|
||||
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS auth_config (
|
||||
user_id TEXT,
|
||||
cookie TEXT,
|
||||
x_bc TEXT,
|
||||
app_token TEXT,
|
||||
x_of_rev TEXT,
|
||||
user_agent TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS download_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT,
|
||||
post_id TEXT,
|
||||
media_id TEXT,
|
||||
media_type TEXT,
|
||||
filename TEXT,
|
||||
downloaded_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS download_cursors (
|
||||
user_id TEXT UNIQUE,
|
||||
cursor TEXT,
|
||||
posts_downloaded INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
);
|
||||
`);
|
||||
|
||||
// Migration: add posted_at column if missing
|
||||
const cols = db.prepare("PRAGMA table_info(download_history)").all().map((c) => c.name);
|
||||
if (!cols.includes('posted_at')) {
|
||||
db.exec('ALTER TABLE download_history ADD COLUMN posted_at TEXT');
|
||||
}
|
||||
|
||||
export function getAuthConfig() {
|
||||
const row = db.prepare('SELECT * FROM auth_config LIMIT 1').get();
|
||||
return row || null;
|
||||
}
|
||||
|
||||
export function saveAuthConfig(config) {
|
||||
const del = db.prepare('DELETE FROM auth_config');
|
||||
const ins = db.prepare(
|
||||
'INSERT INTO auth_config (user_id, cookie, x_bc, app_token, x_of_rev, user_agent) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
|
||||
const upsert = db.transaction((c) => {
|
||||
del.run();
|
||||
ins.run(c.user_id, c.cookie, c.x_bc, c.app_token, c.x_of_rev, c.user_agent);
|
||||
});
|
||||
|
||||
upsert(config);
|
||||
}
|
||||
|
||||
export function isMediaDownloaded(mediaId) {
|
||||
const row = db.prepare('SELECT 1 FROM download_history WHERE media_id = ? LIMIT 1').get(String(mediaId));
|
||||
return !!row;
|
||||
}
|
||||
|
||||
export function recordDownload(userId, postId, mediaId, mediaType, filename, postedAt) {
|
||||
db.prepare(
|
||||
'INSERT INTO download_history (user_id, post_id, media_id, media_type, filename, downloaded_at, posted_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
).run(String(userId), String(postId), String(mediaId), mediaType, filename, new Date().toISOString(), postedAt || null);
|
||||
}
|
||||
|
||||
export function getDownloadHistory(userId) {
|
||||
return db.prepare('SELECT * FROM download_history WHERE user_id = ? ORDER BY downloaded_at DESC').all(String(userId));
|
||||
}
|
||||
|
||||
export function saveCursor(userId, cursor, postsDownloaded) {
|
||||
db.prepare(
|
||||
'INSERT INTO download_cursors (user_id, cursor, posts_downloaded) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET cursor = excluded.cursor, posts_downloaded = excluded.posts_downloaded'
|
||||
).run(String(userId), cursor, postsDownloaded);
|
||||
}
|
||||
|
||||
export function getCursor(userId) {
|
||||
return db.prepare('SELECT cursor, posts_downloaded FROM download_cursors WHERE user_id = ?').get(String(userId)) || null;
|
||||
}
|
||||
|
||||
export function clearCursor(userId) {
|
||||
db.prepare('DELETE FROM download_cursors WHERE user_id = ?').run(String(userId));
|
||||
}
|
||||
|
||||
export function getPostDateByFilename(filename) {
|
||||
const row = db.prepare('SELECT posted_at FROM download_history WHERE filename = ? LIMIT 1').get(filename);
|
||||
return row?.posted_at || null;
|
||||
}
|
||||
|
||||
export function getSetting(key) {
|
||||
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key);
|
||||
return row ? row.value : null;
|
||||
}
|
||||
|
||||
export function setSetting(key, value) {
|
||||
db.prepare(
|
||||
'INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value'
|
||||
).run(key, value);
|
||||
}
|
||||
|
||||
export function getAllSettings() {
|
||||
const rows = db.prepare('SELECT key, value FROM settings').all();
|
||||
const obj = {};
|
||||
for (const row of rows) obj[row.key] = row.value;
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function getDownloadStats() {
|
||||
return db.prepare(
|
||||
'SELECT user_id, COUNT(*) as file_count, MAX(downloaded_at) as last_download FROM download_history GROUP BY user_id'
|
||||
).all();
|
||||
}
|
||||
Reference in New Issue
Block a user