From 93587317f200ff8008a192bb838ef37d28717fca Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Mon, 22 Dec 2025 17:12:00 -0500 Subject: [PATCH 1/5] Adding in more changes --- README.md | 2 ++ src/services/faq/faqService.ts | 25 +++++++++++++++++++++++++ src/services/faq/store.ts | 1 + src/services/faq/type.ts | 2 ++ 4 files changed, 30 insertions(+) create mode 100644 src/services/faq/faqService.ts create mode 100644 src/services/faq/store.ts diff --git a/README.md b/README.md index 2757fdc8..41606347 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,8 @@ OmegaBot is online │ │ │ ├── fetchChannelMessages.ts │ │ │ └── interactionHandler.ts │ │ ├── faq +│ │ │ ├── faqService.ts +│ │ │ ├── store.ts │ │ │ └── type.ts │ │ ├── github │ │ │ ├── githubApi.ts diff --git a/src/services/faq/faqService.ts b/src/services/faq/faqService.ts new file mode 100644 index 00000000..a4a55c12 --- /dev/null +++ b/src/services/faq/faqService.ts @@ -0,0 +1,25 @@ +//src/services/faq/services.ts + +export function getAll() { + +} + +export function getByKey(key: string) { + +} + +export function create(entryInput) { + +} + +export function update(key: string, patch) { + +} + +export function remove(key: string) { + +} + +export function incrementUsage(key: string) { + +} diff --git a/src/services/faq/store.ts b/src/services/faq/store.ts new file mode 100644 index 00000000..e469ec46 --- /dev/null +++ b/src/services/faq/store.ts @@ -0,0 +1 @@ +//src/services/faq/store.ts \ No newline at end of file diff --git a/src/services/faq/type.ts b/src/services/faq/type.ts index 6fbc3131..96da0bb0 100644 --- a/src/services/faq/type.ts +++ b/src/services/faq/type.ts @@ -1,3 +1,5 @@ +//src/services/faq/types.ts + export type FaqEntry = { key: string; title: string; From ba908db9771a1f1c0f6a2619ca1c7e3386ce7846 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Mon, 22 Dec 2025 17:12:05 -0500 Subject: [PATCH 2/5] style: auto-format with Prettier [skip-precheck] --- src/services/faq/faqService.ts | 24 ++++++------------------ src/services/faq/store.ts | 2 +- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/services/faq/faqService.ts b/src/services/faq/faqService.ts index a4a55c12..d32a06f0 100644 --- a/src/services/faq/faqService.ts +++ b/src/services/faq/faqService.ts @@ -1,25 +1,13 @@ //src/services/faq/services.ts -export function getAll() { +export function getAll() {} -} +export function getByKey(key: string) {} -export function getByKey(key: string) { +export function create(entryInput) {} -} +export function update(key: string, patch) {} -export function create(entryInput) { +export function remove(key: string) {} -} - -export function update(key: string, patch) { - -} - -export function remove(key: string) { - -} - -export function incrementUsage(key: string) { - -} +export function incrementUsage(key: string) {} diff --git a/src/services/faq/store.ts b/src/services/faq/store.ts index e469ec46..b5c53bc3 100644 --- a/src/services/faq/store.ts +++ b/src/services/faq/store.ts @@ -1 +1 @@ -//src/services/faq/store.ts \ No newline at end of file +//src/services/faq/store.ts From 8d4b1d39ce51d258625241629159d94c444474f9 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 23 Dec 2025 12:18:30 -0500 Subject: [PATCH 3/5] Adding in FAQ storage files --- README.md | 21 +++- src/services/faq/faqService.ts | 180 +++++++++++++++++++++++++++++++-- src/services/faq/store.ts | 95 ++++++++++++++++- src/services/faq/type.ts | 18 ---- src/services/faq/types.ts | 74 ++++++++++++++ 5 files changed, 361 insertions(+), 27 deletions(-) delete mode 100644 src/services/faq/type.ts create mode 100644 src/services/faq/types.ts diff --git a/README.md b/README.md index 41606347..9c51fd0e 100644 --- a/README.md +++ b/README.md @@ -133,12 +133,31 @@ OmegaBot is online │ │ └── OmegaBot.yml │ └── pull_request_template.md ├── .husky +│ ├── _ +│ │ ├── .gitignore +│ │ ├── applypatch-msg +│ │ ├── commit-msg +│ │ ├── h +│ │ ├── husky.sh +│ │ ├── post-applypatch +│ │ ├── post-checkout +│ │ ├── post-commit +│ │ ├── post-merge +│ │ ├── post-rewrite +│ │ ├── pre-applypatch +│ │ ├── pre-auto-gc +│ │ ├── pre-commit +│ │ ├── pre-merge-commit +│ │ ├── pre-push +│ │ ├── pre-rebase +│ │ └── prepare-commit-msg │ ├── pre-commit │ └── pre-push ├── assets │ ├── banner.png │ └── omegabot.png ├── data +│ ├── faqs.json │ ├── last-seen.json │ └── timezones.json ├── docs @@ -185,7 +204,7 @@ OmegaBot is online │ │ ├── faq │ │ │ ├── faqService.ts │ │ │ ├── store.ts -│ │ │ └── type.ts +│ │ │ └── types.ts │ │ ├── github │ │ │ ├── githubApi.ts │ │ │ ├── githubClient.ts diff --git a/src/services/faq/faqService.ts b/src/services/faq/faqService.ts index d32a06f0..a4d77149 100644 --- a/src/services/faq/faqService.ts +++ b/src/services/faq/faqService.ts @@ -1,13 +1,179 @@ -//src/services/faq/services.ts +// src/services/faq/services.ts -export function getAll() {} +import { loadStore, saveStore } from "./store.js"; +import { logger } from "../../utils/logger.js"; +import { MAX_KEY_LEN, type CreateFaqInput, type FaqEntry } from "./types.js"; -export function getByKey(key: string) {} +/* -------------------------------------------------------------------------- */ +/* Public API */ +/* -------------------------------------------------------------------------- */ -export function create(entryInput) {} +/** + * Return all FAQ entries. + * + * Note: + * - Returns values only (does not guarantee order). + * - Caller can sort if needed (ex: by usageCount or updatedAt). + */ +export function getAll(): FaqEntry[] { + const store = loadStore(); + return Object.values(store.entries); +} -export function update(key: string, patch) {} +/** + * Lookup an FAQ by key. + * + * The key is normalized the same way as create/update/remove. + */ +export function getByKey(key: string): FaqEntry | null { + const store = loadStore(); + const k = assertValidKey(key); + return store.entries[k] ?? null; +} -export function remove(key: string) {} +/** + * Create a new FAQ entry. + * + * Throws if: + * - key normalizes to empty + * - key is too long + * - key already exists + */ +export function create(input: CreateFaqInput): FaqEntry { + const store = loadStore(); -export function incrementUsage(key: string) {} + const key = assertValidKey(input.key); + if (store.entries[key]) { + throw new Error(`FAQ with this key already exists: ${key}`); + } + + const now = new Date().toISOString(); + + const entry: FaqEntry = { + key, + title: input.title.trim(), + body: input.body.trim(), + tags: normalizeTags(input.tags ?? []), + createdAt: now, + updatedAt: now, + createdBy: input.actor, + updatedBy: input.actor, + usageCount: 0, + }; + + store.entries[key] = entry; + saveStore(store); + + return entry; +} + +/** + * Allowed update fields for an FAQ. + * actor is required so we can attribute updatedBy. + */ +export type UpdateFaqPatch = Partial> & { + actor: string; +}; + +/** + * Update an existing FAQ entry by key. + * + * Throws if: + * - key invalid + * - entry not found + */ +export function update(key: string, patch: UpdateFaqPatch): FaqEntry { + const k = assertValidKey(key); + + const store = loadStore(); + const existing = store.entries[k]; + + if (!existing) { + throw new Error(`FAQ not found: ${k}`); + } + + // Apply patch (only touch provided fields). + if (patch.title !== undefined) existing.title = patch.title.trim(); + if (patch.body !== undefined) existing.body = patch.body.trim(); + if (patch.tags !== undefined) existing.tags = normalizeTags(patch.tags); + + existing.updatedAt = new Date().toISOString(); + existing.updatedBy = patch.actor; + + store.entries[k] = existing; + saveStore(store); + + return existing; +} + +/** + * Remove an FAQ by key. + * + * Returns: + * - true if removed + * - false if it did not exist + */ +export function remove(key: string): boolean { + const store = loadStore(); + const k = assertValidKey(key); + + if (!store.entries[k]) return false; + + delete store.entries[k]; + saveStore(store); + return true; +} + +/* -------------------------------------------------------------------------- */ +/* Helpers */ +/* -------------------------------------------------------------------------- */ + +/** + * Normalize an FAQ key into a stable URL-ish slug. + * + * Example: + * "How do I reset my token?" -> "how-do-i-reset-my-token" + */ +function normalizeKey(key: string): string { + return key + .trim() + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^a-z0-9-_]/g, ""); +} + +/** + * Validate and return the normalized key. + * + * Keep this throwing: + * - Callers can catch at the command layer and reply nicely. + * - Services stay strict and predictable. + */ +function assertValidKey(key: string): string { + const k = normalizeKey(key); + + if (k.length === 0) { + throw new Error("FAQ key is empty after normalization"); + } + + if (k.length > MAX_KEY_LEN) { + throw new Error(`FAQ key too long (max ${MAX_KEY_LEN})`); + } + + return k; +} + +/** + * Normalize tags for consistency: + * - trim + * - lowercase + * - remove empties + * - de-dupe + */ +function normalizeTags(tags: string[]): string[] { + const cleaned = tags + .map((t) => t.trim().toLowerCase()) + .filter((t) => t.length > 0); + + return Array.from(new Set(cleaned)); +} \ No newline at end of file diff --git a/src/services/faq/store.ts b/src/services/faq/store.ts index b5c53bc3..150b1fc4 100644 --- a/src/services/faq/store.ts +++ b/src/services/faq/store.ts @@ -1 +1,94 @@ -//src/services/faq/store.ts +// src/services/faq/store.ts + +import fs from "fs"; +import path from "path"; +import { logger } from "../../utils/logger.js"; +import type { FaqStoreV1 } from "./types.js"; + +/** + * Absolute path to the FAQ data file. + * Stored outside src/ so it persists across builds. + */ +const STORE_PATH = path.join(process.cwd(), "data", "faqs.json"); + +/** + * Empty FAQ store template (schema v1). + * Used on first run or when recovery is required. + */ +const EMPTY_STORE: FaqStoreV1 = { + version: 1, + entries: {}, +}; + +/** + * Ensure the FAQ store file exists on disk. + * + * - Creates parent directories if missing + * - Writes an empty store if the file does not exist + * - Safe to call multiple times + */ +export function ensureStoreFile(): void { + if (fs.existsSync(STORE_PATH)) { + return; + } + + fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true }); + fs.writeFileSync( + STORE_PATH, + JSON.stringify(EMPTY_STORE, null, 2), + "utf8", + ); + + logger.info("[faq] created empty FAQ store"); +} + +/** + * Load the FAQ store from disk. + * + * Behavior: + * - Ensures the store file exists + * - Parses JSON from disk + * - Validates basic schema shape + * - Falls back to an empty store on error + */ +export function loadStore(): FaqStoreV1 { + ensureStoreFile(); + + try { + const raw = fs.readFileSync(STORE_PATH, "utf8"); + const parsed = JSON.parse(raw) as FaqStoreV1; + + if ( + parsed.version !== 1 || + typeof parsed.entries !== "object" || + parsed.entries === null + ) { + throw new Error("Invalid FAQ store shape"); + } + + return parsed; + } catch (err) { + logger.error( + { err }, + "[faq] failed to load store, falling back to empty store", + ); + + return { + version: 1, + entries: {}, + }; + } +} + +/** + * Persist the FAQ store back to disk. + * + * Overwrites the entire store atomically. + */ +export function saveStore(store: FaqStoreV1): void { + fs.writeFileSync( + STORE_PATH, + JSON.stringify(store, null, 2), + "utf8", + ); +} \ No newline at end of file diff --git a/src/services/faq/type.ts b/src/services/faq/type.ts deleted file mode 100644 index 96da0bb0..00000000 --- a/src/services/faq/type.ts +++ /dev/null @@ -1,18 +0,0 @@ -//src/services/faq/types.ts - -export type FaqEntry = { - key: string; - title: string; - body: string; - tags: string[]; - createdAt: string; - updatedAt: string; - createdBy: string; - updatedBy: string; - usageCount: number; -}; - -export type FaqStoreV1 = { - version: 1; - entries: Record; -}; diff --git a/src/services/faq/types.ts b/src/services/faq/types.ts new file mode 100644 index 00000000..41c64cb1 --- /dev/null +++ b/src/services/faq/types.ts @@ -0,0 +1,74 @@ +// src/services/faq/types.ts + +/* -------------------------------------------------------------------------- */ +/* Constants */ +/* -------------------------------------------------------------------------- */ + +/** + * Maximum length of an FAQ key after normalization. + * + * Keys are used as: + * - slash command arguments + * - map keys in the JSON store + * - human-readable identifiers + * + * Keeping this short prevents abuse and awkward UX. + */ +export const MAX_KEY_LEN = 48; + +/* -------------------------------------------------------------------------- */ +/* Core Types */ +/* -------------------------------------------------------------------------- */ + +/** + * A single FAQ entry as stored and returned by the service layer. + * + * Notes: + * - `key` is normalized (lowercase, kebab-case). + * - timestamps are ISO strings for easy JSON storage. + * - usageCount is incremented when an FAQ is retrieved. + */ +export type FaqEntry = { + key: string; + title: string; + body: string; + tags: string[]; + + createdAt: string; + updatedAt: string; + + createdBy: string; // user id or "system" + updatedBy: string; // user id or "system" + + usageCount: number; +}; + +/** + * On-disk FAQ store format (v1). + * + * Versioning lets us migrate later without breaking users. + */ +export type FaqStoreV1 = { + version: 1; + entries: Record; +}; + +/* -------------------------------------------------------------------------- */ +/* Input Types */ +/* -------------------------------------------------------------------------- */ + +/** + * Input required to create a new FAQ. + * + * Validation rules: + * - key is normalized and validated by the service layer + * - title/body are trimmed + * - tags are optional and normalized if provided + */ +export type CreateFaqInput = { + key: string; + title: string; + body: string; + tags?: string[]; + actor: string; // user id or "system" +}; \ No newline at end of file From 689bb325b31573e4936e2010d479c3023fbdf886 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 23 Dec 2025 12:18:39 -0500 Subject: [PATCH 4/5] style: auto-format with Prettier [skip-precheck] --- src/services/faq/faqService.ts | 6 ++---- src/services/faq/store.ts | 19 ++++--------------- src/services/faq/types.ts | 2 +- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/services/faq/faqService.ts b/src/services/faq/faqService.ts index a4d77149..ac55ea98 100644 --- a/src/services/faq/faqService.ts +++ b/src/services/faq/faqService.ts @@ -171,9 +171,7 @@ function assertValidKey(key: string): string { * - de-dupe */ function normalizeTags(tags: string[]): string[] { - const cleaned = tags - .map((t) => t.trim().toLowerCase()) - .filter((t) => t.length > 0); + const cleaned = tags.map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0); return Array.from(new Set(cleaned)); -} \ No newline at end of file +} diff --git a/src/services/faq/store.ts b/src/services/faq/store.ts index 150b1fc4..691376f8 100644 --- a/src/services/faq/store.ts +++ b/src/services/faq/store.ts @@ -33,11 +33,7 @@ export function ensureStoreFile(): void { } fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true }); - fs.writeFileSync( - STORE_PATH, - JSON.stringify(EMPTY_STORE, null, 2), - "utf8", - ); + fs.writeFileSync(STORE_PATH, JSON.stringify(EMPTY_STORE, null, 2), "utf8"); logger.info("[faq] created empty FAQ store"); } @@ -68,10 +64,7 @@ export function loadStore(): FaqStoreV1 { return parsed; } catch (err) { - logger.error( - { err }, - "[faq] failed to load store, falling back to empty store", - ); + logger.error({ err }, "[faq] failed to load store, falling back to empty store"); return { version: 1, @@ -86,9 +79,5 @@ export function loadStore(): FaqStoreV1 { * Overwrites the entire store atomically. */ export function saveStore(store: FaqStoreV1): void { - fs.writeFileSync( - STORE_PATH, - JSON.stringify(store, null, 2), - "utf8", - ); -} \ No newline at end of file + fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2), "utf8"); +} diff --git a/src/services/faq/types.ts b/src/services/faq/types.ts index 41c64cb1..011a7c87 100644 --- a/src/services/faq/types.ts +++ b/src/services/faq/types.ts @@ -71,4 +71,4 @@ export type CreateFaqInput = { body: string; tags?: string[]; actor: string; // user id or "system" -}; \ No newline at end of file +}; From 50934b997aa36590062fc88d775b4f347014b419 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 23 Dec 2025 12:20:12 -0500 Subject: [PATCH 5/5] Removing logger from faqService.ts for now as it is not being used --- src/services/faq/faqService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/faq/faqService.ts b/src/services/faq/faqService.ts index ac55ea98..32b59ae7 100644 --- a/src/services/faq/faqService.ts +++ b/src/services/faq/faqService.ts @@ -1,7 +1,6 @@ // src/services/faq/services.ts import { loadStore, saveStore } from "./store.js"; -import { logger } from "../../utils/logger.js"; import { MAX_KEY_LEN, type CreateFaqInput, type FaqEntry } from "./types.js"; /* -------------------------------------------------------------------------- */