import {createHash} from 'node:crypto'; /** * Otomatik e-posta bildirimi abonelikleri (SQLite). * Kullanıcı kendi filtre kriterlerini kaydeder; zamanlayıcı yeni eşleşen * ihaleleri bulup özet e-posta gönderir. */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; let DatabaseSync = null; try { ({ DatabaseSync } = await import('node:sqlite')); } catch { console.warn('[notify-store] node:sqlite yok — otomatik bildirim devre dışı'); } const __dirname = path.dirname(fileURLToPath(import.meta.url)); const DATA_DIR = process.env.IHALEIST_USER_DATA_DIR || path.join(__dirname, 'data'); const DB_PATH = path.join(DATA_DIR, 'notify.sqlite'); const SCHEMA = ` CREATE TABLE IF NOT EXISTS abonelikler ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL, ad TEXT, sehir TEXT, sektor TEXT, alim_turu TEXT, anahtar TEXT, kurumlar TEXT, -- JSON: favori kurum listesi konular TEXT, -- JSON: konu başlıkları (kullanıcı seçer) aktif INTEGER NOT NULL DEFAULT 1, sonGonderim TEXT, sonGorulenler TEXT, -- JSON: son gönderimde bildirilen ihale id'leri olusturma TEXT, criteria_key TEXT NOT NULL, UNIQUE(email, criteria_key) ); CREATE INDEX IF NOT EXISTS idx_abone_email ON abonelikler(email); `; export function available() { return Boolean(DatabaseSync); } function open({ create = true } = {}) { if (!DatabaseSync) return null; if (!fs.existsSync(DB_PATH)) { if (!create) return null; fs.mkdirSync(DATA_DIR, { recursive: true, mode:0o700 }); } const db = new DatabaseSync(DB_PATH); try { fs.chmodSync(DB_PATH,0o600); db.exec('PRAGMA busy_timeout=5000'); db.exec('BEGIN IMMEDIATE'); try { db.exec(SCHEMA); const columns=db.prepare('PRAGMA table_info(abonelikler)').all().map(column=>column.name); if(!columns.includes('konular'))db.exec('ALTER TABLE abonelikler ADD COLUMN konular TEXT'); if(!columns.includes('criteria_key')) { const rows=db.prepare('SELECT * FROM abonelikler ORDER BY id').all(); const sequence=db.prepare("SELECT seq FROM sqlite_sequence WHERE name='abonelikler'").get()?.seq || 0; db.exec('ALTER TABLE abonelikler RENAME TO abonelikler_legacy'); db.exec(SCHEMA); const names=['id','email','ad','sehir','sektor','alim_turu','anahtar','kurumlar','konular','aktif','sonGonderim','sonGorulenler','olusturma','criteria_key']; const insert=db.prepare(`INSERT INTO abonelikler (${names.join(',')}) VALUES (${names.map(()=>'?').join(',')})`); const used=new Set(); for(const row of rows) { const key=subscriptionKey({sehir:row.sehir,sektor:row.sektor,alimTuru:row.alim_turu,anahtar:row.anahtar, konular:JSON.parse(row.konular || '[]'),kurumlar:JSON.parse(row.kurumlar || '[]')}); const identity=JSON.stringify([row.email,key]); // Preserve legacy duplicates and their delivery history; do not discard records. row.criteria_key=used.has(identity)?key+':legacy:'+row.id:key;used.add(identity); insert.run(...names.map(name=>row[name] ?? null)); } db.exec('DROP TABLE abonelikler_legacy'); db.exec('CREATE INDEX IF NOT EXISTS idx_abone_email ON abonelikler(email)'); const current=db.prepare("SELECT seq FROM sqlite_sequence WHERE name='abonelikler'").get(); if(current)db.prepare("UPDATE sqlite_sequence SET seq=? WHERE name='abonelikler'").run(Math.max(sequence,current.seq)); else db.prepare('INSERT INTO sqlite_sequence(name,seq) VALUES (?,?)').run('abonelikler',sequence); } db.exec('COMMIT'); }catch(error){db.exec('ROLLBACK');throw error;} return db; }catch(error){db.close();throw error;} } export function subscriptionKey(criteria) { const normal=value=>String(value || '').trim().toLocaleLowerCase('tr-TR'); const set=values=>{if(!Array.isArray(values))throw new Error('Abonelik konu/kurum listesi geçersiz.');return [...new Set(values.map(normal))].sort();}; return createHash('sha256').update(JSON.stringify([ normal(criteria.sehir),normal(criteria.sektor),normal(criteria.alimTuru),normal(criteria.anahtar),set(criteria.konular || []),set(criteria.kurumlar || []) ])).digest('hex'); } const bosaNull = (v) => (v == null || v === '' ? '' : String(v).trim()); export function subscribe(email, kriter = {}, ad = null) { if(!kriter||typeof kriter!=='object'||Array.isArray(kriter))throw new Error('Geçersiz bildirim kriterleri.'); for(const key of ['konular','kurumlar'])if(kriter[key]!==undefined&&(!Array.isArray(kriter[key])||kriter[key].length>100||kriter[key].some(value=>typeof value!=='string'||!value.trim()||value.length>300)))throw new Error('Geçersiz konu veya kurum listesi.'); for(const key of ['sehir','sektor','alimTuru','anahtar'])if(kriter[key]!==undefined&&(typeof kriter[key]!=='string'||kriter[key].length>300))throw new Error('Geçersiz filtre değeri.'); const db = open(); if (!db) throw new Error('Bildirim deposu kullanılamıyor'); try { db.prepare(` INSERT INTO abonelikler (email, ad, sehir, sektor, alim_turu, anahtar, kurumlar, konular, aktif, olusturma, criteria_key) VALUES (?,?,?,?,?,?,?,?,1,?,?) ON CONFLICT(email, criteria_key) DO UPDATE SET aktif=1, kurumlar=excluded.kurumlar, konular=excluded.konular, ad=excluded.ad `).run( email, ad, bosaNull(kriter.sehir), bosaNull(kriter.sektor), bosaNull(kriter.alimTuru), bosaNull(kriter.anahtar), JSON.stringify((kriter.kurumlar || []).map(value=>value.trim())), JSON.stringify((kriter.konular || []).map(value=>value.trim())), new Date().toISOString(), subscriptionKey(kriter) ); return list(email); } finally { db.close(); } } export function list(email) { const db = open({ create: false }); if (!db) return []; try { const rows = email ? db.prepare('SELECT * FROM abonelikler WHERE email=? ORDER BY id DESC').all(email) : db.prepare('SELECT * FROM abonelikler WHERE aktif=1 ORDER BY id').all(); return rows.map((r) => ({ id: r.id, email: r.email, ad: r.ad, sehir: r.sehir || null, sektor: r.sektor || null, alimTuru: r.alim_turu || null, anahtar: r.anahtar || null, kurumlar: (() => { try { return JSON.parse(r.kurumlar || '[]'); } catch { return []; } })(), konular: (() => { try { return JSON.parse(r.konular || '[]'); } catch { return []; } })(), aktif: Boolean(r.aktif), sonGonderim: r.sonGonderim, sonGorulenler: (() => { try { return JSON.parse(r.sonGorulenler || '[]'); } catch { return []; } })(), })); } finally { db.close(); } } export function remove(id, email) { const db = open({ create: false }); if (!db) return false; try { const r = db.prepare('DELETE FROM abonelikler WHERE id=? AND email=?').run(id, email); return r.changes > 0; } finally { db.close(); } } export function markSent(id, ihaleIdleri) { const db = open({ create: false }); if (!db) return; try { db.prepare('UPDATE abonelikler SET sonGonderim=?, sonGorulenler=? WHERE id=?') .run(new Date().toISOString(), JSON.stringify(ihaleIdleri.slice(0, 400)), id); } finally { db.close(); } }