From e3257dae87e1339ec53153aa8d921ec7545be019 Mon Sep 17 00:00:00 2001 From: Froogal Date: Mon, 29 Dec 2025 15:53:06 -0700 Subject: [PATCH] Revert "Personal improvements" --- README.md | 8 - assets/404/1680564645_404.jpg | Bin 143296 -> 0 bytes constants/numbers.ts | 1 - constants/strings.ts | 53 ++---- db/migration.ts | 39 +---- db/sql/create_settings_table.sql | 1 - db/sql/misc/get_latest_entry_id.sql | 2 +- deno.json | 3 +- deno.lock | 33 ---- handlers/delete_account.ts | 15 +- handlers/gad7_assessment.ts | 226 +++++++++++------------- handlers/new_entry.ts | 261 ++++++++++++---------------- handlers/new_journal_entry.ts | 37 ++-- handlers/phq9_assessment.ts | 226 +++++++++++------------- handlers/register.ts | 152 ++++------------ handlers/set_404_image.ts | 150 ---------------- handlers/view_entries.ts | 71 ++++---- handlers/view_journal_entries.ts | 15 +- main.ts | 176 +++++-------------- models/entry.ts | 218 +++++++++++++---------- models/gad7_score.ts | 213 +++++++---------------- models/journal.ts | 165 ++++++++++-------- models/journal_entry_photo.ts | 22 ++- models/phq9_score.ts | 83 +++++---- models/settings.ts | 134 +++++--------- models/user.ts | 82 ++++++--- tests/dbutils_test.ts | 27 +-- tests/entry_test.ts | 4 +- tests/gad7_score_test.ts | 1 - tests/journal_test.ts | 3 +- tests/migration_test.ts | 19 -- tests/settings_test.ts | 23 --- types/types.ts | 1 - utils/KittyEngine.ts | 5 +- utils/dbHelper.ts | 26 --- utils/dbUtils.ts | 22 +-- utils/keyboards.ts | 7 +- utils/logger.ts | 19 -- utils/misc.ts | 64 +++++-- utils/retry.ts | 131 -------------- 40 files changed, 980 insertions(+), 1758 deletions(-) delete mode 100644 assets/404/1680564645_404.jpg delete mode 100644 handlers/set_404_image.ts delete mode 100644 utils/dbHelper.ts delete mode 100644 utils/logger.ts delete mode 100644 utils/retry.ts diff --git a/README.md b/README.md index 6f5da49..0d361a8 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,6 @@ entries. You can also delete entries from this screen. If you are wanting to stop using Jotbot you can delete your account using /delete_account this will also delete all of your journal entries! -## Configuration - -The bot can be configured using environment variables in a `.env` file: - -- `TELEGRAM_BOT_KEY`: Your Telegram bot token (required) -- `TELEGRAM_API_BASE_URL`: Custom Telegram Bot API base URL (optional, defaults - to `https://api.telegram.org`) - ## Commands **/start** - Start the bot, if it's your first time messaging the bot you will diff --git a/assets/404/1680564645_404.jpg b/assets/404/1680564645_404.jpg deleted file mode 100644 index 9d9af52ec3977e30fd99dd7d56aa8eb748cbba1f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 143296 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%0DWk63Auf6t|Y4em5pg0QN-@KVV3S$ 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 S`im9CW!*j^EZGiz`TyAi@_Q)& diff --git a/constants/numbers.ts b/constants/numbers.ts index db1266b..9d0603a 100644 --- a/constants/numbers.ts +++ b/constants/numbers.ts @@ -1,2 +1 @@ export const pdfFontSize = 30; -export const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10MB limit for file uploads diff --git a/constants/strings.ts b/constants/strings.ts index e19166c..5869f6f 100644 --- a/constants/strings.ts +++ b/constants/strings.ts @@ -1,11 +1,7 @@ export const startString: string = `Hello! Welcome to JotBot. I'm here to help you record your emotions and emotion!`; -// This will be constructed dynamically using the configured API base URL -export const getTelegramDownloadUrl = ( - baseUrl: string, - token: string, - filePath: string, -) => `${baseUrl}/file/bot${token}/${filePath}`; +export const telegramDownloadUrl = + "https://api.telegram.org/file/bot/"; export const catImagesApiBaseUrl = `https://cataas.com`; export const quotesApiBaseUrl = `https://zenquotes.io/api/quotes/`; @@ -41,41 +37,22 @@ export const helpString: string = ` Jotbot is a telegram bot that can help you to record your thoughts and emotions in directly in the telegram app. How do I use Jotbot? +Jotbot is easy to use you have to be "registered" to start recording entries. +Once this is done you can use /new_entry to start recording an entry. You just answer the bot's questions to the best of you ability from there. -🚀 Getting Started: -• Send /start to begin registration -• Follow the prompts to create your profile - -📝 Creating Entries: -• Use /new_entry to start a new journal entry -• Answer 4 simple questions about your thoughts and emotions -• Each step is clearly labeled with examples - -👀 Viewing Entries: -• Use /view_entries to browse your journal -• Navigate with Previous/Next buttons -• Edit or delete entries as needed - -⚙️ Settings: -• Use /settings to customize your experience -• Toggle mental health score saving -• Set a custom 404 image for entries without photos - -🆘 Need Help? -• Use /delete_account to remove all your data +After you are finished recording your entry you can view your entry by using /view_entries. This will bring up a menu that let's you scroll through your entries. You can also delete entries from this screen. +If you are wanting to stop using Jotbot you can delete your account using /delete_account this will also delete all of your journal entries! Commands -/start - Register or access your account -/help - Show this help message -/new_entry - Create a new journal entry (4 simple steps) -/view_entries - Browse and manage your entries -/settings - Customize your bot experience -/kitties - View cute cats for stress relief -/am_i_depressed - Take a depression assessment (PHQ-9) -/am_i_anxious - Take an anxiety assessment (GAD-7) -/snapshot - View your mental health summary -/delete_account - Permanently delete all your data -/🆘 or /sos - Access crisis support resources +/start - Start the bot, if it's your first time messaging the bot you will be asked if you want to register. +/help - Prints this help string in a message +/new_entry - Start a new entry +/view_entries - Scroll through your entries +/kitties - Open the kitties app! Studies show kitties can help with depression +/delete_account - Delete your accound plus all entries +/🆘 or /sos - Show the crisis help lines + +NOTE: The selfie features aren't working right now. `; export enum Emotions { diff --git a/db/migration.ts b/db/migration.ts index f251cbf..79798d0 100644 --- a/db/migration.ts +++ b/db/migration.ts @@ -1,7 +1,6 @@ import { PathLike } from "node:fs"; import { DatabaseSync } from "node:sqlite"; import { sqlFilePath } from "../constants/paths.ts"; -import { logger } from "../utils/logger.ts"; export function createEntryTable(dbFile: PathLike) { try { @@ -12,7 +11,7 @@ export function createEntryTable(dbFile: PathLike) { db.prepare(query).run(); db.close(); } catch (err) { - logger.error(`Failed to create entry_db table: ${err}`); + console.error(`Failed to create entry_db table: ${err}`); } } @@ -26,7 +25,7 @@ export function createGadScoreTable(dbFile: PathLike) { db.prepare(query).run(); db.close(); } catch (err) { - logger.error(`Failed to create gad_score_db table: ${err}`); + console.error(`There was a a problem create the user_db table: ${err}`); } } @@ -40,7 +39,7 @@ export function createPhqScoreTable(dbFile: PathLike) { db.prepare(query).run(); db.close(); } catch (err) { - logger.error(`Failed to create phq_score_db table: ${err}`); + console.error(`There was a a problem create the user_db table: ${err}`); } } @@ -54,7 +53,7 @@ export function createUserTable(dbFile: PathLike) { db.prepare(query).run(); db.close(); } catch (err) { - logger.error(`Failed to create user_db table: ${err}`); + console.error(`There was a a problem create the user_db table: ${err}`); } } @@ -68,7 +67,7 @@ export function createSettingsTable(dbFile: PathLike) { db.prepare(query).run(); db.close(); } catch (err) { - logger.error(`Failed to create settings_db table: ${err}`); + console.error(`Failed to create settings table: ${err}`); } } @@ -82,7 +81,7 @@ export function createJournalTable(dbFile: PathLike) { db.prepare(query).run(); db.close(); } catch (err) { - logger.error(`Failed to create journal_db table: ${err}`); + console.error(`Failed to create settings table: ${err}`); } } @@ -95,7 +94,7 @@ export function createJournalEntryPhotosTable(dbFile: PathLike) { db.prepare(query).run(); db.close(); } catch (err) { - logger.error(`Failed to create photo_db table: ${err}`); + console.error(`Failed to create settings table: ${err}`); } } @@ -109,28 +108,6 @@ export function createVoiceRecordingTable(dbFile: PathLike) { db.prepare(query).run(); db.close(); } catch (err) { - logger.error(`Failed to create voice_recording_db table: ${err}`); - } -} - -export function addCustom404Column(dbFile: PathLike) { - try { - const db = new DatabaseSync(dbFile); - db.exec("PRAGMA foreign_keys = ON;"); - // Check if column exists to avoid errors - const columns = db.prepare("PRAGMA table_info(settings_db);").all() as { - name: string; - }[]; - const hasColumn = columns.some((col) => col.name === "custom404ImagePath"); - if (!hasColumn) { - db.prepare(` - ALTER TABLE settings_db - ADD COLUMN custom404ImagePath TEXT DEFAULT NULL; - `).run(); - logger.info("Added custom404ImagePath column to settings_db"); - } - db.close(); - } catch (err) { - logger.error(`Failed to add custom404ImagePath column: ${err}`); + console.error(`Failed to create settings table: ${err}`); } } diff --git a/db/sql/create_settings_table.sql b/db/sql/create_settings_table.sql index b76b561..07d1411 100644 --- a/db/sql/create_settings_table.sql +++ b/db/sql/create_settings_table.sql @@ -3,6 +3,5 @@ CREATE TABLE IF NOT EXISTS settings_db ( id INTEGER PRIMARY KEY AUTOINCREMENT, userId INTEGER, storeMentalHealthInfo INTEGER DEFAULT 0, - custom404ImagePath TEXT DEFAULT NULL, FOREIGN KEY (userId) REFERENCES user_db(telegramId) ON DELETE CASCADE ); \ No newline at end of file diff --git a/db/sql/misc/get_latest_entry_id.sql b/db/sql/misc/get_latest_entry_id.sql index e638188..6f8feb0 100644 --- a/db/sql/misc/get_latest_entry_id.sql +++ b/db/sql/misc/get_latest_entry_id.sql @@ -1 +1 @@ -SELECT MAX(id) as max_id FROM ; \ No newline at end of file +SELECT seq FROM sqlite_sequence WHERE name=''; \ No newline at end of file diff --git a/deno.json b/deno.json index de28e99..b1997b2 100644 --- a/deno.json +++ b/deno.json @@ -6,10 +6,9 @@ }, "imports": { "@grammyjs/commands": "npm:@grammyjs/commands@^1.2.0", - "@grammyjs/conversations": "npm:@grammyjs/conversations@^1.1.1", + "@grammyjs/conversations": "npm:@grammyjs/conversations@^2.1.1", "@grammyjs/files": "npm:@grammyjs/files@^1.2.0", "@std/assert": "jsr:@std/assert@^1.0.16", - "@std/log": "jsr:@std/log@^0.224.14", "grammy": "npm:grammy@^1.38.4" } } diff --git a/deno.lock b/deno.lock index ddf8cbb..fdc43b7 100644 --- a/deno.lock +++ b/deno.lock @@ -2,16 +2,10 @@ "version": "5", "specifiers": { "jsr:@std/assert@^1.0.16": "1.0.16", - "jsr:@std/fmt@^1.0.5": "1.0.8", - "jsr:@std/fs@^1.0.11": "1.0.21", "jsr:@std/internal@^1.0.12": "1.0.12", - "jsr:@std/io@~0.225.2": "0.225.2", - "jsr:@std/log@*": "0.224.14", - "jsr:@std/log@0.224.14": "0.224.14", "npm:@grammyjs/commands@^1.2.0": "1.2.0_grammy@1.38.4", "npm:@grammyjs/conversations@^2.1.1": "2.1.1_grammy@1.38.4", "npm:@grammyjs/files@^1.2.0": "1.2.0_grammy@1.38.4", - "npm:@types/node@*": "24.2.0", "npm:grammy@^1.38.4": "1.38.4" }, "jsr": { @@ -21,25 +15,8 @@ "jsr:@std/internal" ] }, - "@std/fmt@1.0.8": { - "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" - }, - "@std/fs@1.0.21": { - "integrity": "d720fe1056d78d43065a4d6e0eeb2b19f34adb8a0bc7caf3a4dbf1d4178252cd" - }, "@std/internal@1.0.12": { "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" - }, - "@std/io@0.225.2": { - "integrity": "3c740cd4ee4c082e6cfc86458f47e2ab7cb353dc6234d5e9b1f91a2de5f4d6c7" - }, - "@std/log@0.224.14": { - "integrity": "257f7adceee3b53bb2bc86c7242e7d1bc59729e57d4981c4a7e5b876c808f05e", - "dependencies": [ - "jsr:@std/fmt", - "jsr:@std/fs", - "jsr:@std/io" - ] } }, "npm": { @@ -64,12 +41,6 @@ "@grammyjs/types@3.22.2": { "integrity": "sha512-uu7DX2ezhnBPozL3bXHmwhLvaFsh59E4QyviNH4Cij7EdVekYrs6mCzeXsa2pDk30l3uXo7DBahlZLzTPtpYZg==" }, - "@types/node@24.2.0": { - "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", - "dependencies": [ - "undici-types" - ] - }, "abort-controller@3.0.0": { "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "dependencies": [ @@ -106,9 +77,6 @@ "tr46@0.0.3": { "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "undici-types@7.10.0": { - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" - }, "webidl-conversions@3.0.1": { "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, @@ -224,7 +192,6 @@ "workspace": { "dependencies": [ "jsr:@std/assert@^1.0.16", - "jsr:@std/log@^1.0.16", "npm:@grammyjs/commands@^1.2.0", "npm:@grammyjs/conversations@^2.1.1", "npm:@grammyjs/files@^1.2.0", diff --git a/handlers/delete_account.ts b/handlers/delete_account.ts index 68d89b2..39ea9f7 100644 --- a/handlers/delete_account.ts +++ b/handlers/delete_account.ts @@ -3,13 +3,8 @@ import { Conversation } from "@grammyjs/conversations"; import { deleteAccountConfirmKeyboard } from "../utils/keyboards.ts"; import { deleteUser } from "../models/user.ts"; import { dbFile } from "../constants/paths.ts"; -import { logger } from "../utils/logger.ts"; export async function delete_account(conversation: Conversation, ctx: Context) { - if (!ctx.from) { - await ctx.reply("Error: Unable to identify user."); - return; - } try { await ctx.reply( `⚠️ Are you sure you want to delete your account along with all of your data? ⚠️`, @@ -22,21 +17,21 @@ export async function delete_account(conversation: Conversation, ctx: Context) { ]); if (deleteAccountCtx.callbackQuery.data === "delete-account-yes") { - await conversation.external(() => deleteUser(ctx.from.id, dbFile)); + await conversation.external(() => deleteUser(ctx.from?.id!, dbFile)); } else if (deleteAccountCtx.callbackQuery.data === "delete-account-no") { conversation.halt(); return await deleteAccountCtx.editMessageText("No changes made!"); } await conversation.halt(); return await ctx.editMessageText( - `Okay ${ctx.from.username} your account has been terminated along with all of your entries. Thanks for trying Jotbot!`, + `Okay ${ctx.from?.username} your account has been terminated along with all of your entries. Thanks for trying Jotbot!`, ); } catch (err) { - logger.error( - `Failed to delete user ${ctx.from.username}: ${err}`, + console.log( + `Failed to delete user ${ctx.from?.username}: ${err}`, ); return await ctx.editMessageText( - `Failed to delete user ${ctx.from.username}: ${err}`, + `Failed to delete user ${ctx.from?.username}: ${err}`, ); } } diff --git a/handlers/gad7_assessment.ts b/handlers/gad7_assessment.ts index 1d38801..98b6f22 100644 --- a/handlers/gad7_assessment.ts +++ b/handlers/gad7_assessment.ts @@ -1,7 +1,7 @@ import { Conversation } from "@grammyjs/conversations"; import { Context, InlineKeyboard } from "grammy"; import { gad7Questions } from "../constants/strings.ts"; -import { keyboardFinal, questionnaireKeyboard } from "../utils/keyboards.ts"; +import { keyboardFinal, questionaireKeyboard } from "../utils/keyboards.ts"; import { finalCallBackQueries, questionCallBackQueries, @@ -17,153 +17,127 @@ export async function gad7_assessment( conversation: Conversation, ctx: Context, ) { - try { - if (!ctx.from) { - await ctx.reply("Error: Unable to identify user."); - return; - } - if (!ctx.chatId) { - await ctx.reply("Error: Unable to identify chat."); - return; - } - const ctxMsg = await ctx.api.sendMessage( - ctx.chatId, - `Hello ${ctx.from.username}, this is the Generalized Anxiety Disorder-7 (GAD-7) this test was developed by a team of highly trained mental health professionals + const ctxMsg = await ctx.api.sendMessage( + ctx.chatId!, + `Hello ${ctx.from?.username}, this is the Generalized Anxiety Disorder-7 (GAD-7) this test was developed by a team of highly trained mental health professionals -With that said THIS TEST DOES NOT REPLACE ACTUAL MENTAL HEALTH HELP! If are in serious need of mental health help you should seek help immediately! +With that said THIS TEST DOES NOT REPLACE ACTUAL MENTAL HEALTH HELP! If are in serious need of mental health help you should seek help immediatly! Run /sos to bring up a list of resources that might be able to help -Click here to see the GAD-7 questionnaire itself, this is where the questions are coming from. +Click here to see the PHQ-9 questionaire itself, this is where the questions are coming from. -Do you understand that this test is a simple way to help you gauge your anxiety for your own reference, and is in no way ACTUAL mental health services? +Do you understand that this test is a simple way to help you guage your depression for your own reference, and is in no way ACTUAL mental health services? `, - { - parse_mode: "HTML", - reply_markup: new InlineKeyboard().text("Yes", "gad7-disclaimer-yes") - .text("No", "gad7-disclaimer-no"), - }, - ); - - const phq9DisclaimerCtx = await conversation.waitForCallbackQuery([ - "gad7-disclaimer-yes", - "gad7-disclaimer-no", - ]); - - if (phq9DisclaimerCtx.callbackQuery.data === "gad7-disclaimer-no") { - return await ctx.api.editMessageText( - ctx.chatId, - ctxMsg.message_id, - "No problem! Thanks for checking out the GAD-7 portion of the bot.", - ); - } - - await ctx.api.editMessageText( - ctx.chatId, + { + parse_mode: "HTML", + reply_markup: new InlineKeyboard().text("Yes", "gad7-disclaimer-yes") + .text("No", "gad7-disclaimer-no"), + }, + ); + + const phq9DisclaimerCtx = await conversation.waitForCallbackQuery([ + "gad7-disclaimer-yes", + "gad7-disclaimer-no", + ]); + + if (phq9DisclaimerCtx.callbackQuery.data === "phq9-disclaimer-no") { + return await ctx.api.editMessageText( + ctx.chatId!, ctxMsg.message_id, - `Okay ${ctx.from.username}, let's begin. Over the last 2 weeks how often have you been bother by any of the following problems?`, - { - parse_mode: "HTML", - reply_markup: new InlineKeyboard().text("Begin", "gad7-begin"), - }, + "No problem! Thanks for checking out the GAD-7 portion of the bot.", ); + } - const _gad7BeginCtx = await conversation.waitForCallbackQuery("gad7-begin"); - let gad7Ctx; - let anxietyScore = 0; - - // Questions - for (const question in gad7Questions) { - await ctx.api.editMessageText( - ctx.chatId, - ctxMsg.message_id, - `${Number(question) + 1}. ${gad7Questions[question]}`, - { reply_markup: questionnaireKeyboard, parse_mode: "HTML" }, - ); - gad7Ctx = await conversation.waitForCallbackQuery( - questionCallBackQueries, - ); - switch (gad7Ctx.callbackQuery.data) { - case "not-at-all": { - // No need to add 0 - break; - } - case "several-days": { - anxietyScore += 1; - break; - } - case "more-than-half-the-days": { - anxietyScore += 2; - break; - } - case "nearly-every-day": { - anxietyScore += 3; - break; - } - } - } - + await ctx.api.editMessageText( + ctx.chatId!, + ctxMsg.message_id, + `Okay ${ctx.from?.username}, let's begin. Over the last 2 weeks how often have you been bother by any of the following problems?`, + { + parse_mode: "HTML", + reply_markup: new InlineKeyboard().text("Begin", "gad7-begin"), + }, + ); + + const _gad7BeginCtx = await conversation.waitForCallbackQuery("gad7-begin"); + let gad7Ctx; + let anxietyScore = 0; + + // Questions + for (const question in gad7Questions) { await ctx.api.editMessageText( ctx.chatId!, ctxMsg.message_id, - "If you checked of any problems, how difficult have these problems made it for you to do you work, take care of things at home, or get along with other people?", - { reply_markup: keyboardFinal, parse_mode: "HTML" }, - ); - - const gad7FinalCtx = await conversation.waitForCallbackQuery( - finalCallBackQueries, - ); - - const impactQestionAnswer = gad7FinalCtx.callbackQuery.data; - const gad7Score: GAD7Score = calcGad7Score( - anxietyScore, - ctx.from.id, - impactQestionAnswer, + `${Number(question) + 1}. ${gad7Questions[question]}`, + { reply_markup: questionaireKeyboard, parse_mode: "HTML" }, ); + gad7Ctx = await conversation.waitForCallbackQuery(questionCallBackQueries); + switch (gad7Ctx.callbackQuery.data) { + case "not-at-all": { + // No need to add 0 + break; + } + case "several-days": { + anxietyScore += 1; + break; + } + case "more-than-half-the-days": { + anxietyScore += 2; + break; + } + case "nearly-every-day": { + anxietyScore += 3; + break; + } + } + } - await ctx.api.editMessageText( - ctx.chatId, - ctxMsg.message_id, - `GAD-7 Score: ${gad7Score.score} + await ctx.api.editMessageText( + ctx.chatId!, + ctxMsg.message_id, + "If you checked of any problems, how difficult have these problems made it for you to do you work, take care of things at home, or get along with other people?", + { reply_markup: keyboardFinal, parse_mode: "HTML" }, + ); + + const gad7FinalCtx = await conversation.waitForCallbackQuery( + finalCallBackQueries, + ); + + const impactQestionAnswer = gad7FinalCtx.callbackQuery.data; + const gad7Score: GAD7Score = calcGad7Score( + anxietyScore, + ctx.from?.id!, + impactQestionAnswer, + ); + + await ctx.api.editMessageText( + ctx.chatId!, + ctxMsg.message_id, + `GAD-7 Score: ${gad7Score.score} Anxiety Severity: ${gad7Score.severity} Dealing with this level of anxiety is making your life ${gad7Score.impactQuestionAnswer} ${gad7Score.action}`, - { parse_mode: "HTML" }, - ); - - if (userExists(ctx.from.id, dbFile)) { - const settings = getSettingsById(ctx.from.id, dbFile); - - if (settings?.storeMentalHealthInfo) { - try { - insertGadScore(gad7Score, dbFile); - await ctx.reply("Score saved!"); - } catch (err) { - logger.error( - `Failed to save GAD-7 score for user ${ctx.from.id}: ${err}`, - ); - await ctx.reply("❌ Failed to save score. Please try again."); - } - } else { - await ctx.reply("Scores not saved."); + { parse_mode: "HTML" }, + ); + + if (userExists(ctx.from?.id!, dbFile)) { + const settings = getSettingsById(ctx.from?.id!, dbFile); + + if (settings?.storeMentalHealthInfo) { + try { + insertGadScore(gad7Score, dbFile); + await ctx.reply("Score saved!"); + } catch (err) { + throw err; } } else { - await ctx.reply( - "It looks like you haven't registered, you can register by running /register to store you scores and keep track of your mental health.", - ); + await ctx.reply("Scores not saved."); } - } catch (error) { - logger.error( - `Error in gad7_assessment conversation for user ${ctx.from?.id}: ${error}`, + } else { + await ctx.reply( + "It looks like you haven't registered, you can register by running /register to store you scores and keep track of your mental health.", ); - try { - await ctx.reply( - "❌ Sorry, there was an error during the assessment. Please try again.", - ); - } catch (replyError) { - logger.error(`Failed to send error message: ${replyError}`); - } } } diff --git a/handlers/new_entry.ts b/handlers/new_entry.ts index e183f7e..af5bd24 100644 --- a/handlers/new_entry.ts +++ b/handlers/new_entry.ts @@ -4,167 +4,130 @@ import { Emotion, Entry } from "../types/types.ts"; import { insertEntry } from "../models/entry.ts"; import { telegramDownloadUrl } from "../constants/strings.ts"; import { dbFile } from "../constants/paths.ts"; -import { MAX_FILE_SIZE_BYTES } from "../constants/numbers.ts"; -import { logger } from "../utils/logger.ts"; export async function new_entry(conversation: Conversation, ctx: Context) { - if (!ctx.from) { - await ctx.reply("Error: Unable to identify user."); - return; + // Describe situation + await ctx.api.sendMessage( + ctx.chatId!, + "Describe the situation that brought up your thought.", + ); + const situationCtx = await conversation.waitFor("message:text"); + + // Record automatic thoughts + await ctx.reply( + `Okay ${ctx.from?.username} describe the thought. Rate how much you believed it out of 100%.`, + ); + const automaticThoughtCtx = await conversation.waitFor("message:text"); + + // Emoji and emotion descriptor + await ctx.reply( + "Send one word describing your emotions, along with an emoji that matches your emotions.", + ); + const emojiAndEmotionName = await conversation.waitFor("message:text"); + + // Describe your feelings + await ctx.reply( + "What emotions were you feeling at the time? How intense were your feelings out of 100%?", + ); + const emotionDescriptionCtx = await conversation.waitFor("message:text"); + + // Store emoji and emotion name + const emotionNameAndEmoji = emojiAndEmotionName.message.text.split(" "); + let emotionEmoji: string, emotionName: string; + if (/\p{Emoji}/u.test(emotionNameAndEmoji[0])) { + emotionEmoji = emotionNameAndEmoji[0]; + emotionName = emotionNameAndEmoji[1]; + } else { + emotionEmoji = emotionNameAndEmoji[1]; + emotionName = emotionNameAndEmoji[0]; } - if (!ctx.chatId) { - await ctx.reply("Error: Unable to identify chat."); - return; - } - try { - // Describe situation - await ctx.api.sendMessage( - ctx.chatId, - '📝 Step 1: Describe the Situation\n\nDescribe the situation that brought up your thought.\n\nExample: "I was at work and my boss criticized my presentation."', - { parse_mode: "HTML" }, - ); - const situationCtx = await conversation.waitFor("message:text"); - - // Record automatic thoughts - await ctx.reply( - `🧠 Step 2: Your Automatic Thought\n\nDescribe the thought that came to mind. Then rate how much you believed it (0-100%).\n\nExample: \"I'm terrible at my job. Belief: 85%\"`, - { parse_mode: "HTML" }, - ); - const automaticThoughtCtx = await conversation.waitFor("message:text"); - - // Emoji and emotion descriptor - await ctx.reply( - '😊 Step 3: Your Emotion\n\nSend one word describing your emotion, followed by a matching emoji.\n\nExample: "anxious 😰" or "sad 😢"\n\nThe emoji should represent how you felt.', - { parse_mode: "HTML" }, - ); - const emojiAndEmotionName = await conversation.waitFor("message:text"); - // Describe your feelings - await ctx.reply( - '💭 Step 4: Emotion Description\n\nDescribe the emotions you were feeling and how intense they were (0-100%).\n\nExample: "I felt very anxious and overwhelmed. Intensity: 90%"', - { parse_mode: "HTML" }, - ); - const emotionDescriptionCtx = await conversation.waitFor("message:text"); - - // Store emoji and emotion name - const emotionNameAndEmoji = emojiAndEmotionName.message.text.split(" "); - let emotionEmoji: string, emotionName: string; - if (/\p{Emoji}/u.test(emotionNameAndEmoji[0])) { - emotionEmoji = emotionNameAndEmoji[0]; - emotionName = emotionNameAndEmoji[1]; - } else { - emotionEmoji = emotionNameAndEmoji[1]; - emotionName = emotionNameAndEmoji[0]; - } + // Build emotion object + const emotion: Emotion = { + emotionName: emotionName, + emotionEmoji: emotionEmoji, + emotionDescription: emotionDescriptionCtx.message.text, + }; - // Build emotion object - const emotion: Emotion = { - emotionName: emotionName, - emotionEmoji: emotionEmoji, - emotionDescription: emotionDescriptionCtx.message.text, - }; - - const askSelfieMsg = await ctx.reply("Would you like to take a selfie?", { - reply_markup: new InlineKeyboard().text("✅ Yes", "selfie-yes").text( - "⛔ No", - "selfie-no", - ), - }); - - const selfieCtx = await conversation.waitForCallbackQuery([ - "selfie-yes", + const askSelfieMsg = await ctx.reply("Would you like to take a selfie?", { + reply_markup: new InlineKeyboard().text("✅ Yes", "selfie-yes").text( + "⛔ No", "selfie-no", - ]); - - let selfiePath: string | null = ""; - if (selfieCtx.callbackQuery.data === "selfie-yes") { - try { - await ctx.api.editMessageText( - ctx.chatId, - askSelfieMsg.message_id, - "Send me a selfie.", - ); - const selfiePathCtx = await conversation.waitFor("message:photo"); - - const tmpFile = await selfiePathCtx.getFile(); - if (!tmpFile.file_path) { - throw new Error("File path is missing from Telegram response"); - } - if (tmpFile.file_size && tmpFile.file_size > MAX_FILE_SIZE_BYTES) { - await ctx.reply( - `❌ File too large! Maximum size is 10MB. Your file is ${ - (tmpFile.file_size / (1024 * 1024)).toFixed(2) - }MB.`, - ); - } - const selfieResponse = await fetch( - telegramDownloadUrl.replace("", ctx.api.token).replace( - "", - tmpFile.file_path, - ), - ); - if (selfieResponse.body) { - await conversation.external(async () => { // use conversation.external - const fileName = `${ctx.from?.id}_${ - new Date(Date.now()).toLocaleString() - }.jpg`.replaceAll(" ", "_").replace(",", "").replaceAll("/", "-"); // Build and sanitize selfie file name - - const filePath = `${Deno.cwd()}/assets/selfies/${fileName}`; - const file = await Deno.open(filePath, { - write: true, - create: true, - }); - - logger.debug(`Saving selfie file: ${filePath}`); - selfiePath = await Deno.realPath(filePath); - await selfieResponse.body.pipeTo(file.writable); - }); + ), + }); - await ctx.reply(`Selfie saved successfully!`); - } - } catch (err) { - logger.error(`Failed to save selfie: ${err}`); - } - } else if (selfieCtx.callbackQuery.data === "selfie-no") { - selfiePath = null; - } else { - logger.error( - `Invalid callback query selection: ${selfieCtx.callbackQuery.data}`, + const selfieCtx = await conversation.waitForCallbackQuery([ + "selfie-yes", + "selfie-no", + ]); + + let selfiePath: string | null = ""; + if (selfieCtx.callbackQuery.data === "selfie-yes") { + try { + await ctx.api.editMessageText( + ctx.chatId!, + askSelfieMsg.message_id, + "Send me a selfie.", ); - } + const selfiePathCtx = await conversation.waitFor("message:photo"); + + const tmpFile = await selfiePathCtx.getFile(); + // console.log(selfiePathCtx.message.c); + const selfieResponse = await fetch( + telegramDownloadUrl.replace("", ctx.api.token).replace( + "", + tmpFile.file_path!, + ), + ); + if (selfieResponse.body) { + await conversation.external(async () => { // use conversation.external + const fileName = `${ctx.from?.id}_${ + new Date(Date.now()).toLocaleString() + }.jpg`.replaceAll(" ", "_").replace(",", "").replaceAll("/", "-"); // Build and sanitize selfie file name + + const filePath = `${Deno.cwd()}/assets/selfies/${fileName}`; + const file = await Deno.open(filePath, { + write: true, + create: true, + }); - const entry: Entry = { - timestamp: await conversation.external(() => Date.now()), - userId: ctx.from.id, - emotion: emotion, - situation: situationCtx.message.text, - automaticThoughts: automaticThoughtCtx.message.text, - selfiePath: selfiePath, - }; + console.log(`File: ${file}`); + selfiePath = await Deno.realPath(filePath); + await selfieResponse.body!.pipeTo(file.writable); + }); - try { - await conversation.external(() => insertEntry(entry, dbFile)); + await ctx.reply(`Selfie saved successfully!`); + } } catch (err) { - logger.error(`Failed to insert entry: ${err}`); - return await ctx.reply(`Failed to insert entry: ${err}`); + console.log(`Jotbot Error: Failed to save selfie: ${err}`); } - - return await ctx.reply( - `Entry added at ${ - new Date(entry.timestamp).toLocaleString() - }! Thank you for logging your emotion with me.`, - ); - } catch (error) { - logger.error( - `Error in new_entry conversation for user ${ctx.from?.id}: ${error}`, + } else if (selfieCtx.callbackQuery.data === "selfie-no") { + selfiePath = null; + } else { + console.log( + `Invalid Selection: ${selfieCtx.callbackQuery.data}`, ); - try { - await ctx.reply( - "❌ Sorry, there was an error creating your entry. Please try again with /new_entry.", - ); - } catch (replyError) { - logger.error(`Failed to send error message: ${replyError}`); - } - // Don't rethrow - let the conversation end gracefully } + + const entry: Entry = { + timestamp: await conversation.external(() => Date.now()), + userId: ctx.from?.id!, + emotion: emotion, + situation: situationCtx.message.text, + automaticThoughts: automaticThoughtCtx.message.text, + selfiePath: selfiePath, + }; + + try { + await conversation.external(() => insertEntry(entry, dbFile)); + } catch (err) { + console.log(`Failed to insert Entry: ${err}`); + return await ctx.reply(`Failed to insert entry: ${err}`); + } + + return await ctx.reply( + `Entry added at ${ + new Date(entry.timestamp!).toLocaleString() + }! Thank you for logging your emotion with me.`, + ); } diff --git a/handlers/new_journal_entry.ts b/handlers/new_journal_entry.ts index db6c807..b59d873 100644 --- a/handlers/new_journal_entry.ts +++ b/handlers/new_journal_entry.ts @@ -6,10 +6,8 @@ import { insertJournalEntry, } from "../models/journal.ts"; import { dbFile } from "../constants/paths.ts"; -import { MAX_FILE_SIZE_BYTES } from "../constants/numbers.ts"; import { downloadTelegramImage } from "../utils/misc.ts"; import { insertJournalEntryPhoto } from "../models/journal_entry_photo.ts"; -import { logger } from "../utils/logger.ts"; /** * Starts the process of creating a new journal entry. @@ -20,26 +18,22 @@ export async function new_journal_entry( conversation: Conversation, ctx: Context, ) { - if (!ctx.from) { - await ctx.reply("Error: Unable to identify user."); - return; - } await ctx.reply( - `Hello ${ctx.from.username || "User"}! Tell me what is on your mind.`, + `Hello ${ctx.from?.username!}! Tell me what is on your mind.`, ); const journalEntryCtx = await conversation.waitFor("message:text"); // Try to insert journal entry try { const journalEntry: JournalEntry = { - userId: ctx.from.id, + userId: ctx.from?.id!, timestamp: await conversation.external(() => Date.now()), content: journalEntryCtx.message.text, length: journalEntryCtx.message.text.length, }; await conversation.external(() => insertJournalEntry(journalEntry, dbFile)); } catch (err) { - logger.error(`Failed to insert Journal Entry: ${err}`); + console.error(`Failed to insert Journal Entry: ${err}`); await ctx.reply(`Failed to insert Journal Entry: ${err}`); throw new Error(`Failed to insert Journal Entry: ${err}`); } @@ -63,36 +57,29 @@ export async function new_journal_entry( try { const file = await imagesCtx.getFile(); - if (file.file_size && file.file_size > MAX_FILE_SIZE_BYTES) { - await ctx.reply( - `❌ File too large! Maximum size is 10MB. Your file is ${ - (file.file_size / (1024 * 1024)).toFixed(2) - }MB.`, - ); - continue; - } - const journalEntries = await conversation.external(() => - getAllJournalEntriesByUserId(ctx.from.id, dbFile) + const id = await conversation.external(() => + getAllJournalEntriesByUserId(ctx.from?.id!, dbFile)[0].id! ); - const id = journalEntries[0]?.id ?? 0; - const caption = imagesCtx.message?.caption; const journalEntryPhoto = await conversation.external(async () => await downloadTelegramImage( ctx.api.token, - caption ?? "", + imagesCtx.message?.caption!, file, id, // Latest ID ) ); - logger.debug(`Journal entry photo: ${JSON.stringify(journalEntryPhoto)}`); + console.log(journalEntryPhoto); await conversation.external(() => insertJournalEntryPhoto(journalEntryPhoto, dbFile) ); await ctx.reply(`Saved photo!`); imageCount++; } catch (err) { - logger.error( - `Failed to save images for Journal Entry: ${err}`, + console.error( + `Failed to save images for Journal Entry ${getAllJournalEntriesByUserId( + ctx.from?.id!, + dbFile, + )[0].id!}: ${err}`, ); } } diff --git a/handlers/phq9_assessment.ts b/handlers/phq9_assessment.ts index 101c36c..e3664c6 100644 --- a/handlers/phq9_assessment.ts +++ b/handlers/phq9_assessment.ts @@ -1,6 +1,6 @@ import { Context, InlineKeyboard } from "grammy"; import { Conversation } from "@grammyjs/conversations"; -import { keyboardFinal, questionnaireKeyboard } from "../utils/keyboards.ts"; +import { keyboardFinal, questionaireKeyboard } from "../utils/keyboards.ts"; import { phq9Questions } from "../constants/strings.ts"; import { PHQ9Score } from "../types/types.ts"; import { calcPhq9Score } from "../utils/misc.ts"; @@ -17,153 +17,127 @@ export async function phq9_assessment( conversation: Conversation, ctx: Context, ) { - try { - if (!ctx.from) { - await ctx.reply("Error: Unable to identify user."); - return; - } - if (!ctx.chatId) { - await ctx.reply("Error: Unable to identify chat."); - return; - } - const ctxMsg = await ctx.api.sendMessage( - ctx.chatId, - `Hello ${ctx.from.username}, this is the Patient Health Questionaire-9 (PHQ-9) this test was developed by a team of highly trained mental health professionals + const ctxMsg = await ctx.api.sendMessage( + ctx.chatId!, + `Hello ${ctx.from?.username}, this is the Patient Health Questionaire-9 (PHQ-9) this test was developed by a team of highly trained mental health professionals -With that said THIS TEST DOES NOT REPLACE ACTUAL MENTAL HEALTH HELP! If are in serious need of mental health help you should seek help immediately! +With that said THIS TEST DOES NOT REPLACE ACTUAL MENTAL HEALTH HELP! If are in serious need of mental health help you should seek help immediatly! Run /sos to bring up a list of resources that might be able to help -Click here to see the PHQ-9 questionnaire itself, this is where the questions are coming from. +Click here to see the PHQ-9 questionaire itself, this is where the questions are coming from. -Do you understand that this test is a simple way to help you gauge your depression for your own reference, and is in no way ACTUAL mental health services? +Do you understand that this test is a simple way to help you guage your depression for your own reference, and is in no way ACTUAL mental health services? `, - { - parse_mode: "HTML", - reply_markup: new InlineKeyboard().text("Yes", "phq9-disclaimer-yes") - .text("No", "phq9-disclaimer-no"), - }, + { + parse_mode: "HTML", + reply_markup: new InlineKeyboard().text("Yes", "phq9-disclaimer-yes") + .text("No", "phq9-disclaimer-no"), + }, + ); + + const phq9DisclaimerCtx = await conversation.waitForCallbackQuery([ + "phq9-disclaimer-yes", + "phq9-disclaimer-no", + ]); + + if (phq9DisclaimerCtx.callbackQuery.data === "phq9-disclaimer-no") { + return await ctx.api.editMessageText( + ctx.chatId!, + ctxMsg.message_id, + "No problem! Thanks for checking out the phq\-9 portion of the bot.", ); + } - const phq9DisclaimerCtx = await conversation.waitForCallbackQuery([ - "phq9-disclaimer-yes", - "phq9-disclaimer-no", - ]); - - if (phq9DisclaimerCtx.callbackQuery.data === "phq9-disclaimer-no") { - return await ctx.api.editMessageText( - ctx.chatId, - ctxMsg.message_id, - "No problem! Thanks for checking out the phq\-9 portion of the bot.", - ); - } - + await ctx.api.editMessageText( + ctx.chatId!, + ctxMsg.message_id, + `Okay ${ctx.from?.username}, let's begin. Over the last 2 weeks how often have you been bother by any of the following problems?`, + { + parse_mode: "HTML", + reply_markup: new InlineKeyboard().text("Begin", "phq9-begin"), + }, + ); + + const _phq9BeginCtx = await conversation.waitForCallbackQuery("phq9-begin"); + let phq9Ctx; + let depressionScore = 0; + + // Questions + for (const question in phq9Questions) { await ctx.api.editMessageText( - ctx.chatId, + ctx.chatId!, ctxMsg.message_id, - `Okay ${ctx.from.username}, let's begin. Over the last 2 weeks how often have you been bother by any of the following problems?`, - { - parse_mode: "HTML", - reply_markup: new InlineKeyboard().text("Begin", "phq9-begin"), - }, + phq9Questions[question], + { reply_markup: questionaireKeyboard, parse_mode: "HTML" }, ); - - const _phq9BeginCtx = await conversation.waitForCallbackQuery("phq9-begin"); - let phq9Ctx; - let depressionScore = 0; - - // Questions - for (const question in phq9Questions) { - await ctx.api.editMessageText( - ctx.chatId, - ctxMsg.message_id, - phq9Questions[question], - { reply_markup: questionnaireKeyboard, parse_mode: "HTML" }, - ); - phq9Ctx = await conversation.waitForCallbackQuery( - questionCallBackQueries, - ); - switch (phq9Ctx.callbackQuery.data) { - case "not-at-all": { - // No need to add 0 - break; - } - case "several-days": { - depressionScore += 1; - break; - } - case "more-than-half-the-days": { - depressionScore += 2; - break; - } - case "nearly-every-day": { - depressionScore += 3; - break; - } + phq9Ctx = await conversation.waitForCallbackQuery(questionCallBackQueries); + switch (phq9Ctx.callbackQuery.data) { + case "not-at-all": { + // No need to add 0 + break; + } + case "several-days": { + depressionScore += 1; + break; + } + case "more-than-half-the-days": { + depressionScore += 2; + break; + } + case "nearly-every-day": { + depressionScore += 3; + break; } } + } - await ctx.api.editMessageText( - ctx.chatId, - ctxMsg.message_id, - "If you checked of any problems, how difficult have these problems made it for you to do you work, take care of things at home, or get along with other people?", - { reply_markup: keyboardFinal, parse_mode: "HTML" }, - ); - - const phq9FinalCtx = await conversation.waitForCallbackQuery( - finalCallBackQueries, - ); - const impactQestionAnswer = phq9FinalCtx.callbackQuery.data; - const phq9Score: PHQ9Score = calcPhq9Score( - depressionScore, - ctx.from.id, - impactQestionAnswer, - ); - - await ctx.api.editMessageText( - ctx.chatId, - ctxMsg.message_id, - `PHQ-9 Score: ${phq9Score.score} + await ctx.api.editMessageText( + ctx.chatId!, + ctxMsg.message_id, + "If you checked of any problems, how difficult have these problems made it for you to do you work, take care of things at home, or get along with other people?", + { reply_markup: keyboardFinal, parse_mode: "HTML" }, + ); + + const phq9FinalCtx = await conversation.waitForCallbackQuery( + finalCallBackQueries, + ); + const impactQestionAnswer = phq9FinalCtx.callbackQuery.data; + const phq9Score: PHQ9Score = calcPhq9Score( + depressionScore, + ctx.from?.id!, + impactQestionAnswer, + ); + + await ctx.api.editMessageText( + ctx.chatId!, + ctxMsg.message_id, + `PHQ-9 Score: ${phq9Score.score} Depression Severity: ${phq9Score.severity} Dealing with this level of depression is making your life ${phq9Score.impactQuestionAnswer} ${phq9Score.action}`, - { parse_mode: "HTML" }, - ); - - if (userExists(ctx.from.id, dbFile)) { - const settings = getSettingsById(ctx.from.id, dbFile); - - if (settings?.storeMentalHealthInfo) { - try { - phq9Score.id = ctx.from.id; - insertPhqScore(phq9Score, dbFile); - await ctx.reply("Score saved!"); - } catch (err) { - logger.error( - `Failed to save PHQ-9 score for user ${ctx.from.id}: ${err}`, - ); - await ctx.reply("❌ Failed to save score. Please try again."); - } - } else { - await ctx.reply("Scores not saved."); + { parse_mode: "HTML" }, + ); + + if (userExists(ctx.from?.id!, dbFile)) { + const settings = getSettingsById(ctx.from?.id!, dbFile); + + if (settings?.storeMentalHealthInfo) { + try { + phq9Score.id = ctx.from?.id!; + insertPhqScore(phq9Score, dbFile); + await ctx.reply("Score saved!"); + } catch (err) { + await ctx.reply(`Failed to save score: ${err}`); } } else { - await ctx.reply( - "It looks like you haven't registered, you can register by running /register to store you scores and keep track of your mental health.", - ); + await ctx.reply("Scores not saved."); } - } catch (error) { - logger.error( - `Error in phq9_assessment conversation for user ${ctx.from?.id}: ${error}`, + } else { + await ctx.reply( + "It looks like you haven't registered, you can register by running /register to store you scores and keep track of your mental health.", ); - try { - await ctx.reply( - "❌ Sorry, there was an error during the assessment. Please try again.", - ); - } catch (replyError) { - logger.error(`Failed to send error message: ${replyError}`); - } } } diff --git a/handlers/register.ts b/handlers/register.ts index 220f842..90c8076 100644 --- a/handlers/register.ts +++ b/handlers/register.ts @@ -3,130 +3,46 @@ import { Conversation } from "@grammyjs/conversations"; import { insertUser } from "../models/user.ts"; import { User } from "../types/types.ts"; import { dbFile } from "../constants/paths.ts"; -import { logger } from "../utils/logger.ts"; - -function isValidDate(input: string): { isValid: boolean; message?: string } { - const trimmedInput = input.trim(); - - const regex = /^\d{4}\/\d{2}\/\d{2}$/; - if (!regex.test(trimmedInput)) { - return { isValid: false, message: "Invalid format. Please use YYYY/MM/DD" }; - } - - const [year, month, day] = trimmedInput.split("/").map(Number); - - const date = new Date(year, month - 1, day); - if (isNaN(date.getTime())) { - return { - isValid: false, - message: "Invalid date. Please enter a valid date.", - }; - } - - if ( - date.getFullYear() !== year || date.getMonth() + 1 !== month || - date.getDate() !== day - ) { - return { - isValid: false, - message: "Invalid date. Please check your input and try again.", - }; - } - - const now = new Date(); - const minDate = new Date( - now.getFullYear() - 120, - now.getMonth(), - now.getDate(), - ); - const minAgeDate = new Date( - now.getFullYear() - 13, - now.getMonth(), - now.getDate(), - ); - - if (date > now) { - return { isValid: false, message: "Date cannot be in the future." }; - } - - if (date < minDate) { - return { - isValid: false, - message: "Date cannot be more than 120 years in the past.", - }; - } - - if (date > minAgeDate) { - return { - isValid: false, - message: "You must be at least 13 years old to use this bot.", - }; - } - - return { isValid: true }; -} export async function register(conversation: Conversation, ctx: Context) { - if (!ctx.from) { - await ctx.reply("Error: Unable to identify user."); - return; - } + let dob; try { - let dob; - try { - while (true) { - await ctx.editMessageText( - `Okay ${ctx.from.username} what is your date of birth? YYYY/MM/DD`, - ); - const dobCtx = conversation.waitFor("message:text"); - const inputText = (await dobCtx).message.text.trim(); - const validation = isValidDate(inputText); + while (true) { + await ctx.editMessageText( + `Okay ${ctx.from?.username} what is your date of birth? YYYY/MM/DD`, + ); + const dobCtx = conversation.waitFor("message:text"); + dob = new Date((await dobCtx).message.text); - if (!validation.isValid) { - (await dobCtx).reply(`${validation.message} Please try again.`); - } else { - dob = new Date(inputText); - break; - } + if (isNaN(dob.getTime())) { + (await dobCtx).reply("Invalid date entered. Please try again."); + } else { + break; } - } catch (err) { - logger.error(`Error getting DOB for user ${ctx.from.id}: ${err}`); - await ctx.reply( - "❌ Sorry, there was an error processing your date of birth. Please try registering again with /start.", - ); - return; // End conversation gracefully } + } catch (err) { + await ctx.reply(`Failed to save birthdate: ${err}`); + throw new Error(`Failed to save birthdate: ${err}`); + } - const user: User = { - telegramId: ctx.from.id, - username: ctx.from.username || "User", - dob: dob, - joinedDate: await conversation.external(() => { - return new Date(Date.now()); - }), - }; + const user: User = { + telegramId: ctx.from?.id!, + username: ctx.from?.username!, + dob: dob, + joinedDate: await conversation.external(() => { + return new Date(Date.now()); + }), + }; - logger.debug(`Registering new user: ${JSON.stringify(user)}`); - try { - insertUser(user, dbFile); - } catch (err) { - ctx.reply(`Failed to save user ${user.username}: ${err}`); - logger.error(`Error inserting user ${user.username}: ${err}`); - } - await ctx.reply( - `Welcome ${user.username}! You have been successfully registered. Would you like to start by recording an entry?`, - { reply_markup: new InlineKeyboard().text("New Entry", "new-entry") }, - ); - } catch (error) { - logger.error( - `Error in register conversation for user ${ctx.from?.id}: ${error}`, - ); - try { - await ctx.reply( - "❌ Sorry, there was an error during registration. Please try again with /start.", - ); - } catch (replyError) { - logger.error(`Failed to send error message: ${replyError}`); - } - } + console.log(user); + try { + insertUser(user, dbFile); + } catch (err) { + ctx.reply(`Failed to save user ${user.username}: ${err}`); + console.log(`Error inserting user ${user.username}: ${err}`); + } + ctx.reply( + `Welcome ${user.username}! You have been successfully registered. Would you like to start by recording an entry?`, + { reply_markup: new InlineKeyboard().text("New Entry", "new-entry") }, + ); } diff --git a/handlers/set_404_image.ts b/handlers/set_404_image.ts deleted file mode 100644 index a36a39e..0000000 --- a/handlers/set_404_image.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { Context } from "grammy"; -import { Conversation } from "@grammyjs/conversations"; -import { updateCustom404Image } from "../models/settings.ts"; -import { getTelegramDownloadUrl } from "../constants/strings.ts"; -import { dbFile } from "../constants/paths.ts"; -import { logger } from "../utils/logger.ts"; - -export async function set_404_image(conversation: Conversation, ctx: Context) { - logger.info(`Starting 404 image setup for user ${ctx.from?.id}`); - - await ctx.reply( - "🖼️ Set Custom 404 Image\n\nSend me an image that will be shown when viewing journal entries that don't have selfies.\n\nThis image will be displayed as a placeholder for entries without photos.\n\nSend to image now:", - { parse_mode: "HTML" }, - ); - - logger.debug(`Waiting for photo from user ${ctx.from?.id}`); - const photoCtx = await conversation.waitFor("message:photo"); - logger.debug(`Received photo message: ${!!photoCtx.message.photo}`); - - if (!photoCtx.message.photo) { - logger.warn(`No photo in message from user ${ctx.from?.id}`); - await ctx.reply("No photo received. Operation cancelled."); - return; - } - - const photo = photoCtx.message.photo[photoCtx.message.photo.length - 1]; // Get largest - logger.debug( - `Selected largest photo: file_id=${photo.file_id}, size=${photo.file_size}`, - ); - - logger.debug(`Getting file info for ${photo.file_id}`); - let tmpFile; - try { - tmpFile = await ctx.api.getFile(photo.file_id); - logger.debug( - `File info received: path=${tmpFile.file_path}, size=${tmpFile.file_size}`, - ); - } catch (error) { - logger.error(`Failed to get file info: ${error}`); - await ctx.reply( - "❌ Failed to process the image. Please try uploading again.", - ); - return; - } - - if (tmpFile.file_size && tmpFile.file_size > 5_000_000) { // 5MB limit - logger.warn(`File too large: ${tmpFile.file_size} bytes`); - await ctx.reply( - "Image is too large (max 5MB). Please try a smaller image.", - ); - return; - } - - // Extract relative file path from absolute server path - // Telegram API expects paths like "photos/file_0.jpg", not "/var/lib/telegram-bot-api/.../photos/file_0.jpg" - const relativeFilePath = tmpFile.file_path!; - if ( - relativeFilePath.includes("/photos/") || - relativeFilePath.includes("/documents/") || - relativeFilePath.includes("/videos/") - ) { - // Find the last occurrence of known Telegram file directories - const photoIndex = relativeFilePath.lastIndexOf("/photos/"); - const docIndex = relativeFilePath.lastIndexOf("/documents/"); - const videoIndex = relativeFilePath.lastIndexOf("/videos/"); - - const lastIndex = Math.max(photoIndex, docIndex, videoIndex); - if (lastIndex !== -1) { - relativeFilePath.substring(lastIndex + 1); - } - } - - logger.debug(`Using relative file path: ${relativeFilePath}`); - - try { - const baseUrl = (ctx.api as { options?: { apiRoot?: string } }).options - ?.apiRoot || - "https://api.telegram.org"; - const downloadUrl = getTelegramDownloadUrl( - baseUrl, - ctx.api.token, - relativeFilePath, - ); - - logger.debug(`Base URL: ${baseUrl}`); - logger.debug(`Download URL: ${downloadUrl}`); - - logger.debug(`Starting fetch request...`); - let response = await fetch(downloadUrl, { - signal: AbortSignal.timeout(30000), // 30 second timeout - }); - - logger.debug( - `Fetch response: status=${response.status}, ok=${response.ok}`, - ); - - // If custom API fails, try official API as fallback - if (!response.ok && baseUrl !== "https://api.telegram.org") { - logger.info( - `Custom API failed, trying official Telegram API as fallback...`, - ); - const officialUrl = getTelegramDownloadUrl( - "https://api.telegram.org", - ctx.api.token, - relativeFilePath, - ); - logger.debug(`Official URL: ${officialUrl}`); - - response = await fetch(officialUrl, { - signal: AbortSignal.timeout(30000), - }); - - logger.debug( - `Official response: status=${response.status}, ok=${response.ok}`, - ); - } - - if (!response.ok) { - const errorText = await response.text().catch(() => "No error text"); - logger.error( - `Download failed: status=${response.status}, body="${errorText}"`, - ); - throw new Error(`HTTP ${response.status}: ${errorText}`); - } - - const fileName = `${ctx.from?.id}_404.jpg`; - const filePath = `assets/404/${fileName}`; - logger.debug(`Saving to: ${filePath}`); - - const file = await Deno.open(filePath, { - write: true, - create: true, - }); - - logger.debug(`Starting file download...`); - await response.body!.pipeTo(file.writable); - logger.debug(`File download completed`); - - // Update settings - logger.debug(`Updating database settings`); - updateCustom404Image(ctx.from!.id, filePath, dbFile); - logger.debug(`Settings updated successfully`); - - await ctx.reply("✅ 404 image set successfully!"); - logger.info(`404 image setup completed for user ${ctx.from?.id}`); - } catch (err) { - logger.error(`Failed to set 404 image: ${err}`); - await ctx.reply("❌ Failed to set 404 image. Please try again."); - } -} diff --git a/handlers/view_entries.ts b/handlers/view_entries.ts index 873eaa6..3885b41 100644 --- a/handlers/view_entries.ts +++ b/handlers/view_entries.ts @@ -10,37 +10,21 @@ import { viewEntriesKeyboard } from "../utils/keyboards.ts"; import { entryFromString } from "../utils/misc.ts"; import { InputFile } from "grammy/types"; import { dbFile } from "../constants/paths.ts"; -import { getSettingsById } from "../models/settings.ts"; -import { logger } from "../utils/logger.ts"; export async function view_entries(conversation: Conversation, ctx: Context) { - if (!ctx.from) { - await ctx.reply("Error: Unable to identify user."); - return; - } - if (!ctx.chatId) { - await ctx.reply("Error: Unable to identify chat."); - return; - } let entries: Entry[] = await conversation.external(() => - getAllEntriesByUserId(ctx.from.id, dbFile) + getAllEntriesByUserId(ctx.from?.id!, dbFile) ); // If there are no stored entries inform user and stop conversation if (entries.length === 0) { - return await ctx.api.sendMessage(ctx.chatId, "No entries to view."); + return await ctx.api.sendMessage(ctx.chatId!, "No entries to view."); } - // Get user settings for custom 404 image - const settings = await conversation.external(() => - getSettingsById(ctx.from.id, dbFile) - ); - const default404Image = settings?.custom404ImagePath || "assets/404.png"; - let currentEntry: number = 0; let lastEditedTimestampString = `Last Edited ${ entries[currentEntry].lastEditedTimestamp - ? new Date(entries[currentEntry].lastEditedTimestamp).toLocaleString() + ? new Date(entries[currentEntry].lastEditedTimestamp!).toLocaleString() : "" }`; let selfieCaptionString = `Page ${ @@ -48,7 +32,7 @@ export async function view_entries(conversation: Conversation, ctx: Context) { } of ${entries.length} Date Created ${ - new Date(entries[currentEntry].timestamp).toLocaleString() + new Date(entries[currentEntry].timestamp!).toLocaleString() } ${entries[currentEntry].lastEditedTimestamp ? lastEditedTimestampString : ""} Emotion @@ -70,11 +54,11 @@ Page ${currentEntry + 1} of ${entries.length} // Reply initially with first entry before starting loop const displaySelfieMsg = await ctx.replyWithPhoto( - new InputFile(entries[currentEntry].selfiePath || default404Image), + new InputFile(entries[currentEntry].selfiePath! || "assets/404.png"), { caption: selfieCaptionString, parse_mode: "HTML" }, ); - const displayEntryMsg = await ctx.api.sendMessage(ctx.chatId, entryString, { + const displayEntryMsg = await ctx.api.sendMessage(ctx.chatId!, entryString, { reply_markup: viewEntriesKeyboard, parse_mode: "HTML", }); @@ -119,7 +103,7 @@ Page ${currentEntry + 1} of ${entries.length} } case "delete-entry": { await ctx.api.editMessageText( - ctx.chatId, + ctx.chatId!, displayEntryMsg.message_id, "Are you sure you want to delete this entry?", { @@ -142,18 +126,18 @@ Page ${currentEntry + 1} of ${entries.length} // Delete selfie file associated with entry if (entries[currentEntry].selfiePath) { await conversation.external(async () => { - await Deno.remove(entries[currentEntry].selfiePath); + await Deno.remove(entries[currentEntry].selfiePath!); }); } // Delete the current entry await conversation.external(() => - deleteEntryById(entries[currentEntry].id, dbFile) + deleteEntryById(entries[currentEntry].id!, dbFile) ); // Refresh entries array entries = await conversation.external(() => - getAllEntriesByUserId(ctx.from.id, dbFile) + getAllEntriesByUserId(ctx.from?.id!, dbFile) ); if (entries.length === 0) { @@ -170,7 +154,7 @@ Page ${currentEntry + 1} of ${entries.length} } case "view-entry-backbutton": { // Close view entries menu - await ctx.api.deleteMessages(ctx.chatId, [ + await ctx.api.deleteMessages(ctx.chatId!, [ displayEntryMsg.message_id, displaySelfieMsg.message_id, ]); @@ -178,11 +162,12 @@ Page ${currentEntry + 1} of ${entries.length} } case "edit-entry": { const editEntryMsg = await viewEntryCtx.api.sendMessage( - ctx.chatId, + ctx.chatId!, `Copy the entry from above, edit it and send it back to me.`, ); const editEntryCtx = await conversation.waitFor("message:text"); + // console.log(`Entry to edit: ${editEntryCtx.message.text}`); let entryToEdit: Entry; try { entryToEdit = entryFromString(editEntryCtx.message.text); @@ -192,39 +177,39 @@ Page ${currentEntry + 1} of ${entries.length} return Date.now(); }); - logger.debug(`Entry to edit: ${JSON.stringify(entryToEdit)}`); + console.log(entryToEdit); } catch (err) { await editEntryCtx.reply( `There was an error reading your edited entry. Make sure you are only editing the parts that YOU typed!`, ); - logger.error(`Error reading edited entry: ${err}`); + console.log(err); } - await editEntryCtx.api.deleteMessage(ctx.chatId, editEntryCtx.msgId); + await editEntryCtx.api.deleteMessage(ctx.chatId!, editEntryCtx.msgId); try { await conversation.external(() => - updateEntry(entryToEdit.id, entryToEdit, dbFile) + updateEntry(entryToEdit.id!, entryToEdit, dbFile) ); } catch (err) { await editEntryCtx.reply( `I'm sorry I ran into an error while trying to save your changes.`, ); - logger.error(`Error updating entry: ${err}`); + console.log(err); } // Refresh entries entries = await conversation.external(() => - getAllEntriesByUserId(ctx.from.id, dbFile) + getAllEntriesByUserId(ctx.from?.id!, dbFile) ); - // await viewEntryCtx.api.sendMessage(ctx.chatId, "Entry Updated!"); + // await viewEntryCtx.api.sendMessage(ctx.chatId!, "Entry Updated!"); await ctx.api.editMessageText( - ctx.chatId, + ctx.chatId!, editEntryMsg.message_id, "Message Updated!", ); await ctx.api.deleteMessage( - ctx.chatId, + ctx.chatId!, editEntryMsg.message_id, ); break; @@ -236,9 +221,11 @@ Page ${currentEntry + 1} of ${entries.length} } } + // console.log(entries[currentEntry]); + lastEditedTimestampString = `Last Edited ${ entries[currentEntry].lastEditedTimestamp - ? new Date(entries[currentEntry].lastEditedTimestamp).toLocaleString() + ? new Date(entries[currentEntry].lastEditedTimestamp!).toLocaleString() : "" }`; @@ -247,7 +234,7 @@ Page ${currentEntry + 1} of ${entries.length} } of ${entries.length} Date Created ${ - new Date(entries[currentEntry].timestamp).toLocaleString() + new Date(entries[currentEntry].timestamp!).toLocaleString() } ${entries[currentEntry].lastEditedTimestamp ? lastEditedTimestampString : ""} Emotion @@ -269,17 +256,17 @@ Page ${currentEntry + 1} of ${entries.length} try { await ctx.api.editMessageText( - ctx.chatId, + ctx.chatId!, displayEntryMsg.message_id, entryString, { reply_markup: viewEntriesKeyboard, parse_mode: "HTML" }, ); await ctx.api.editMessageMedia( - ctx.chatId, + ctx.chatId!, displaySelfieMsg.message_id, InputMediaBuilder.photo( - new InputFile(entries[currentEntry].selfiePath || default404Image), + new InputFile(entries[currentEntry].selfiePath! || "assets/404.png"), { caption: selfieCaptionString, parse_mode: "HTML" }, ), ); diff --git a/handlers/view_journal_entries.ts b/handlers/view_journal_entries.ts index 2f121d1..6e4289c 100644 --- a/handlers/view_journal_entries.ts +++ b/handlers/view_journal_entries.ts @@ -1,14 +1,9 @@ import { Conversation } from "@grammyjs/conversations"; import { Context, InlineKeyboard } from "grammy"; -export async function view_journal_entries( - conversation: Conversation, - ctx: Context, -) { - await ctx.reply("Buttons!", { - reply_markup: new InlineKeyboard().text("Add beans"), - }); +export async function view_journal_entries(conversation: Conversation, ctx: Context) { + await ctx.reply('Buttons!', {reply_markup: new InlineKeyboard().text("Add beans")}); - const _otherCtx = await conversation.wait(); - await ctx.reply("Tits"); -} + const otherCtx = await conversation.wait(); + await ctx.reply("Tits"); +} \ No newline at end of file diff --git a/main.ts b/main.ts index 0d23078..b8cb4aa 100644 --- a/main.ts +++ b/main.ts @@ -1,5 +1,4 @@ import { Bot, Context, InlineQueryResultBuilder } from "grammy"; -import { load } from "@std/dotenv"; import { type ConversationFlavor, conversations, @@ -8,6 +7,7 @@ import { import { new_entry } from "./handlers/new_entry.ts"; import { register } from "./handlers/register.ts"; import { existsSync } from "node:fs"; +// import { createEntryTable, createUserTable } from "./db/migration.ts"; import { userExists } from "./models/user.ts"; import { deleteEntryById, getAllEntriesByUserId } from "./models/entry.ts"; import { InlineQueryResult } from "grammy/types"; @@ -28,10 +28,8 @@ import { view_entries } from "./handlers/view_entries.ts"; import { crisisString, helpString } from "./constants/strings.ts"; import { kitties } from "./handlers/kitties.ts"; import { phq9_assessment } from "./handlers/phq9_assessment.ts"; -import { logger } from "./utils/logger.ts"; import { gad7_assessment } from "./handlers/gad7_assessment.ts"; import { new_journal_entry } from "./handlers/new_journal_entry.ts"; -import { set_404_image } from "./handlers/set_404_image.ts"; import { dbFile } from "./constants/paths.ts"; import { createDatabase, getLatestId } from "./utils/dbUtils.ts"; import { getSettingsById, updateSettings } from "./models/settings.ts"; @@ -40,34 +38,18 @@ import { getGadScoreById } from "./models/gad7_score.ts"; import { view_journal_entries } from "./handlers/view_journal_entries.ts"; if (import.meta.main) { - // Load environment variables from .env file if present - await load({ export: true }); - - // Check for required environment variables - const botKey = Deno.env.get("TELEGRAM_BOT_KEY"); - if (!botKey) { - logger.error( - "TELEGRAM_BOT_KEY environment variable is not set. Please set it in .env file or environment.", - ); - Deno.exit(1); - } - logger.info("Bot key loaded successfully"); - - // Get optional Telegram API base URL - const apiBaseUrl = Deno.env.get("TELEGRAM_API_BASE_URL") || - "https://api.telegram.org"; - logger.info(`Using Telegram API base URL: ${apiBaseUrl}`); + // Check if database is present and if not create one // Check if db file exists if not create it and the tables if (!existsSync(dbFile)) { try { - logger.info("No Database Found creating a new database"); + console.log("No Database Found creating a new database"); createDatabase(dbFile); } catch (err) { - logger.error(`Failed to create database: ${err}`); + console.error(`Failed to created database: ${err}`); } } else { - logger.info("Database found! Starting bot."); + console.log("Database found! Starting bot."); } // Check if selfie directory exists and create it if it doesn't @@ -75,17 +57,7 @@ if (import.meta.main) { try { Deno.mkdir("assets/selfies"); } catch (err) { - logger.error(`Failed to create selfie directory: ${err}`); - Deno.exit(1); - } - } - - // Check if 404 images directory exists and create it if it doesn't - if (!existsSync("assets/404")) { - try { - Deno.mkdir("assets/404"); - } catch (err) { - logger.error(`Failed to create 404 images directory: ${err}`); + console.error(`Failed to create selfie directory: ${err}`); Deno.exit(1); } } @@ -98,11 +70,6 @@ if (import.meta.main) { const jotBot = new Bot( Deno.env.get("TELEGRAM_BOT_KEY") || "", - { - client: { - apiRoot: apiBaseUrl, - }, - }, ); jotBot.api.config.use(hydrateFiles(jotBot.token)); const jotBotCommands = new CommandGroup(); @@ -118,28 +85,22 @@ if (import.meta.main) { jotBot.use(createConversation(phq9_assessment)); jotBot.use(createConversation(gad7_assessment)); jotBot.use(createConversation(new_journal_entry)); - jotBot.use(createConversation(set_404_image)); jotBot.use(createConversation(view_journal_entries)); jotBotCommands.command("start", "Starts the bot.", async (ctx) => { // Check if user exists in Database - if (!ctx.from) { - await ctx.reply("Error: Unable to identify user."); - return; - } - const userTelegramId = ctx.from.id; - const username = ctx.from.username || "User"; + const userTelegramId = ctx.from?.id!; if (!userExists(userTelegramId, dbFile)) { ctx.reply( - `Welcome ${username}! I can see you are a new user, would you like to register now?`, + `Welcome ${ctx.from?.username}! I can see you are a new user, would you like to register now?`, { reply_markup: registerKeyboard, }, ); } else { await ctx.reply( - `Hello ${username} you have already completed onboarding process.`, + `Hello ${ctx.from?.username} you have already completed the onboarding process.`, { reply_markup: mainCustomKeyboard }, ); } @@ -170,14 +131,9 @@ if (import.meta.main) { }); jotBotCommands.command("new_entry", "Create new entry", async (ctx) => { - if (!ctx.from) { - await ctx.reply("Error: Unable to identify user."); - return; - } - const username = ctx.from.username || "User"; - if (!userExists(ctx.from.id, dbFile)) { + if (!userExists(ctx.from?.id!, dbFile)) { await ctx.reply( - `Hello ${username}! It looks like you haven't completed the onboarding process yet. Would you like to register to begin the registration process?`, + `Hello ${ctx.from?.username}! It looks like you haven't completed the onboarding process yet. Would you like to register to begin the registration process?`, { reply_markup: registerKeyboard }, ); } else { @@ -189,14 +145,9 @@ if (import.meta.main) { "new_journal_entry", "Create new journal entry", async (ctx) => { - if (!ctx.from) { - await ctx.reply("Error: Unable to identify user."); - return; - } - const username = ctx.from.username || "User"; - if (!userExists(ctx.from.id, dbFile)) { + if (!userExists(ctx.from?.id!, dbFile)) { await ctx.reply( - `Hello ${username}! It looks like you haven't completed the onboarding process yet. Would you like to register to begin the registration process?`, + `Hello ${ctx.from?.username}! It looks like you haven't completed the onboarding process yet. Would you like to register to begin the registration process?`, { reply_markup: registerKeyboard }, ); } else { @@ -209,14 +160,9 @@ if (import.meta.main) { "view_entries", "View current entries.", async (ctx) => { - if (!ctx.from) { - await ctx.reply("Error: Unable to identify user."); - return; - } - const username = ctx.from.username || "User"; - if (!userExists(ctx.from.id, dbFile)) { + if (!userExists(ctx.from?.id!, dbFile)) { await ctx.reply( - `Hello ${username}! It looks like you haven't completed the onboarding process yet. Would you like to register to begin the registration process?`, + `Hello ${ctx.from?.username}! It looks like you haven't completed the onboarding process yet. Would you like to register to begin the registration process?`, { reply_markup: registerKeyboard }, ); } else { @@ -245,16 +191,12 @@ if (import.meta.main) { "delete_entry", "Delete specific entry", async (ctx) => { - if (!ctx.message?.text) { - await ctx.reply("Error: No message text found."); - return; - } let entryId: number = 0; - if (ctx.message.text.split(" ").length < 2) { + if (ctx.message!.text.split(" ").length < 2) { await ctx.reply("Journal ID Not found."); return; } else { - entryId = Number(ctx.message.text.split(" ")[1]); + entryId = Number(ctx.message!.text.split(" ")[1]); } deleteEntryById(entryId, dbFile); @@ -265,8 +207,7 @@ if (import.meta.main) { /(🆘|(?:sos))/, // ?: matches upper or lower case so no matter how sos is typed it will recognize it. "Show helplines and other crisis information.", async (ctx) => { - const username = ctx.from?.username ?? "User"; - await ctx.reply(crisisString.replace("", username), { + await ctx.reply(crisisString.replace("", ctx.from?.username!), { parse_mode: "HTML", }); }, @@ -305,20 +246,19 @@ if (import.meta.main) { // const lastAnxietyScore = getGad; await ctx.reply( `Mental Health Overview -This is an overview of your mental health based on your answers to the GAD-7 and PHQ-9 questionnaires. +This is an overview of your mental health based on your answers to the GAD-7 and PHQ-9 questionaires. This snap shot only shows the last score. THIS IS NOT A MEDICAL OR PSYCIATRIC DIAGNOSIS!! Only a trained mental health professional can diagnose actual mental illness. This is meant to be a personal reference so you may seek help if you feel you need it. - Depression Overview - Last Taken ${ - lastDepressionScore - ? new Date(lastDepressionScore.timestamp).toLocaleString() - : "No data" +Depression Overview +Last Taken ${ + new Date(lastDepressionScore?.timestamp!).toLocaleString() || + "No data" } - Last PHQ-9 Score ${lastDepressionScore?.score || "No Data"} +Last PHQ-9 Score ${lastDepressionScore?.score || "No Data"} Depression Severity ${ lastDepressionScore?.severity.toString() || "No data" } @@ -328,16 +268,14 @@ Only a trained mental health professional can diagnose actual mental illness. T Description ${lastDepressionScore?.action || "No data"} - Anxiety Overview - Last Taken ${ - lastAnxietyScore?.timestamp - ? new Date(lastAnxietyScore.timestamp).toLocaleString() - : "No Data" +Anxietey Overview +Last Taken ${ + new Date(lastAnxietyScore?.timestamp).toLocaleString() || "No Data" } - Last GAD-7 Score ${lastAnxietyScore?.score || "No Data"} - Anxiety Severity ${lastAnxietyScore?.severity || "No data"} - Anxiety impact on my life ${ - lastAnxietyScore?.impactQuestionAnswer || "No data" +Last GAD-7 Score ${lastAnxietyScore?.score || "No Data"} +Anxiety Severity ${lastAnxietyScore?.severity || "No data"} +Anxiety impact on my life ${ + lastAnxietyScore.impactQuestionAnswer || "No data" } Anxiety Description ${lastAnxietyScore?.action || "No data"}`, @@ -350,24 +288,22 @@ ${lastAnxietyScore?.action || "No data"}`, const entries = getAllEntriesByUserId(ctx.inlineQuery.from.id, dbFile); const entriesInlineQueryResults: InlineQueryResult[] = []; for (const entry in entries) { - const entryDate = entries[entry].timestamp - ? new Date(entries[entry].timestamp) - : new Date(0); + const entryDate = new Date(entries[entry].timestamp!); // Build string const entryString = ` - Date ${entryDate.toLocaleString()} - Emotion - ${entries[entry].emotion.emotionName} ${entries[entry].emotion.emotionEmoji} +Date ${entryDate.toLocaleString()} +Emotion +${entries[entry].emotion.emotionName} ${entries[entry].emotion.emotionEmoji} - Emotion Description - ${entries[entry].emotion.emotionDescription} +Emotion Description +${entries[entry].emotion.emotionDescription} - Situation - ${entries[entry].situation} +Situation +${entries[entry].situation} - Automatic Thoughts - ${entries[entry].automaticThoughts} - `; +Automatic Thoughts +${entries[entry].automaticThoughts} +`; entriesInlineQueryResults.push( InlineQueryResultBuilder.article( String(entries[entry].id), @@ -391,20 +327,12 @@ ${lastAnxietyScore?.action || "No data"}`, }); jotBot.callbackQuery( - ["smhs", "set-404-image", "settings-back"], + ["smhs", "settings-back"], async (ctx) => { switch (ctx.callbackQuery.data) { case "smhs": { - if (!ctx.from) { - await ctx.reply("Error: Unable to identify user."); - return; - } - const settings = getSettingsById(ctx.from.id, dbFile); - logger.debug( - `Retrieved settings for user ${ctx.from.id}: ${ - JSON.stringify(settings) - }`, - ); + const settings = getSettingsById(ctx.from?.id!, dbFile); + console.log(settings); if (settings?.storeMentalHealthInfo) { settings.storeMentalHealthInfo = false; await ctx.editMessageText( @@ -415,9 +343,7 @@ ${lastAnxietyScore?.action || "No data"}`, }, ); } else { - if (settings) { - settings.storeMentalHealthInfo = true; - } + settings!.storeMentalHealthInfo = true; await ctx.editMessageText( `I WILL store your GAD-7 and PHQ-9 scores`, { @@ -426,13 +352,7 @@ ${lastAnxietyScore?.action || "No data"}`, }, ); } - if (settings) { - updateSettings(ctx.from.id, settings, dbFile); - } - break; - } - case "set-404-image": { - await ctx.conversation.enter("set_404_image"); + updateSettings(ctx.from?.id!, settings!, dbFile); break; } case "settings-back": { @@ -447,7 +367,7 @@ ${lastAnxietyScore?.action || "No data"}`, ); jotBot.catch((err) => { - logger.error(`JotBot Error: ${err.message}`); + console.log(`JotBot Error: ${err.message}`); }); jotBot.use(jotBotCommands); jotBot.filter(commandNotFound(jotBotCommands)) diff --git a/models/entry.ts b/models/entry.ts index 49f0b7d..9e3bafa 100644 --- a/models/entry.ts +++ b/models/entry.ts @@ -1,8 +1,7 @@ +import { DatabaseSync, SQLOutputValue } from "node:sqlite"; import { Entry } from "../types/types.ts"; import { PathLike } from "node:fs"; import { sqlFilePath } from "../constants/paths.ts"; -import { logger } from "../utils/logger.ts"; -import { withDB } from "../utils/dbHelper.ts"; const sqlFilePathEntry = `${sqlFilePath}/entry`; @@ -13,30 +12,32 @@ const sqlFilePathEntry = `${sqlFilePath}/entry`; * @returns StatementResultingChanges */ export function insertEntry(entry: Entry, dbFile: PathLike) { - return withDB(dbFile, (db) => { - const query = Deno.readTextFileSync(`${sqlFilePathEntry}/insert_entry.sql`) - .trim(); - const queryResult = db.prepare(query).run( - entry.userId, - entry.timestamp, - entry.lastEditedTimestamp || null, - entry.situation, - entry.automaticThoughts, - entry.emotion.emotionName, - entry.emotion.emotionEmoji || null, - entry.emotion.emotionDescription, - entry.selfiePath || null, - ); - - if (queryResult.changes === 0) { - logger.error( - `Failed to insert entry for user ${entry.userId}: No changes made`, - ); - throw new Error("Failed to insert entry: Database returned no changes"); - } + const db = new DatabaseSync(dbFile); + const query = Deno.readTextFileSync(`${sqlFilePathEntry}/insert_entry.sql`) + .trim(); // Grab query from file + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); + const queryResult = db.prepare(query).run( + entry.userId, + entry.timestamp!, + entry.lastEditedTimestamp || null, + entry.situation, + entry.automaticThoughts, + entry.emotion.emotionName, + entry.emotion.emotionEmoji || null, + entry.emotion.emotionDescription, + entry.selfiePath || null, + ); - return queryResult; - }); + if (queryResult.changes === 0) { + throw new Error( + `Query ran but no changes were made.`, + ); + } + db.close(); + return queryResult; } /** @@ -51,35 +52,36 @@ export function updateEntry( updatedEntry: Entry, dbFile: PathLike, ) { - return withDB(dbFile, (db) => { - const queryResult = db.prepare( - `UPDATE OR FAIL entry_db SET - lastEditedTimestamp = ?, - situation = ?, - automaticThoughts = ?, - emotionName = ?, - emotionEmoji = ?, - emotionDescription = ? - WHERE id = ?;`, - ).run( - updatedEntry.lastEditedTimestamp ?? Date.now(), - updatedEntry.situation, - updatedEntry.automaticThoughts, - updatedEntry.emotion.emotionName, - updatedEntry.emotion.emotionEmoji || null, - updatedEntry.emotion.emotionDescription, - entryId, + try { + const db = new DatabaseSync(dbFile); + const query = Deno.readTextFileSync(`${sqlFilePathEntry}/update_entry.sql`) + .replace("", updatedEntry.id!.toString()).trim(); + if ( + !(db.prepare("PRAGMA integrity_check(entry_db);").get() + ?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); + const queryResult = db.prepare(query).run( + updatedEntry.lastEditedTimestamp!, + updatedEntry.situation!, + updatedEntry.automaticThoughts!, + updatedEntry.emotion.emotionName!, + updatedEntry.emotion.emotionEmoji! || null, + updatedEntry.emotion.emotionDescription!, ); if (queryResult.changes === 0) { - logger.error(`Failed to update entry ${entryId}: No changes made`); throw new Error( - `Failed to update entry: Entry ID ${entryId} not found or no changes made`, + `Query ran but no changes were made.`, ); } + db.close(); return queryResult; - }); + } catch (err) { + console.error(`Failed to update entry ${entryId}: ${err}`); + throw new Error(`Failed to update entry ${entryId} in entry_db: ${err}`); + } } /** @@ -89,17 +91,28 @@ export function updateEntry( * @returns StatementResultingChanges | undefined */ export function deleteEntryById(entryId: number, dbFile: PathLike) { - return withDB(dbFile, (db) => { - const queryResult = db.prepare(`DELETE FROM entry_db WHERE id = ?;`).run( - entryId, - ); + try { + const db = new DatabaseSync(dbFile); + const query = Deno.readTextFileSync(`${sqlFilePathEntry}/delete_entry.sql`) + .replace("", entryId.toString()).trim(); + if ( + !(db.prepare("PRAGMA integrity_check(entry_db);").get() + ?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); + const queryResult = db.prepare(query).run(); if (queryResult.changes === 0) { - logger.warn(`No entry found with ID ${entryId} to delete`); + throw new Error( + `Query ran but no changes were made.`, + ); } + db.close(); return queryResult; - }); + } catch (err) { + console.error(`Failed to delete entry ${entryId} from entry_db: ${err}`); + } } /** @@ -107,31 +120,38 @@ export function deleteEntryById(entryId: number, dbFile: PathLike) { * @param dbFile PathLike - Path to the sqlite db file * @returns Entry */ -export function getEntryById( - entryId: number, - dbFile: PathLike, -): Entry | undefined { - return withDB(dbFile, (db) => { - const queryResult = db.prepare(`SELECT * FROM entry_db WHERE id = ?;`).get( - entryId, - ); - if (!queryResult) return undefined; +export function getEntryById(entryId: number, dbFile: PathLike): Entry { + let queryResult: Record | undefined; + try { + const db = new DatabaseSync(dbFile); + const query = Deno.readTextFileSync( + `${sqlFilePathEntry}/get_entry_by_id.sql`, + ).replace("", entryId.toString()).trim(); + if ( + !(db.prepare("PRAGMA integrity_check(entry_db);").get() + ?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); + queryResult = db.prepare(query).get(); + db.close(); + } catch (err) { + console.error(`Failed to retrieve entry: ${entryId}: ${err}`); + } - return { - id: Number(queryResult.id), - userId: Number(queryResult.userId), - timestamp: Number(queryResult.timestamp), - lastEditedTimestamp: Number(queryResult.lastEditedTimestamp) || null, - situation: String(queryResult.situation), - automaticThoughts: String(queryResult.automaticThoughts), - emotion: { - emotionName: String(queryResult.emotionName), - emotionEmoji: String(queryResult.emotionEmoji), - emotionDescription: String(queryResult.emotionDescription), - }, - selfiePath: queryResult.selfiePath?.toString() || null, - }; - }); + return { + id: Number(queryResult?.id!), + userId: Number(queryResult?.userId!), + timestamp: Number(queryResult?.timestamp!), + lastEditedTimestamp: Number(queryResult?.lastEditedTimestamp!) || null, + situation: String(queryResult?.situation!), + automaticThoughts: String(queryResult?.automaticThoughts!), + emotion: { + emotionName: String(queryResult?.emotionName!), + emotionEmoji: String(queryResult?.emotionEmoji!), + emotionDescription: String(queryResult?.emotionDescription!), + }, + selfiePath: queryResult?.selfiePath?.toString() || null, + }; } /** @@ -144,29 +164,39 @@ export function getAllEntriesByUserId( userId: number, dbFile: PathLike, ): Entry[] { - return withDB(dbFile, (db) => { - const queryResults = db.prepare( - `SELECT * FROM entry_db WHERE userId = ? ORDER BY timestamp DESC;`, - ).all(userId); - const entries = []; - for (const result of queryResults) { + const entries = []; + try { + const db = new DatabaseSync(dbFile); + const query = Deno.readTextFileSync( + `${sqlFilePathEntry}/get_all_entries_by_id.sql`, + ).replace("", userId.toString()).trim(); + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + const queryResults = db.prepare(query).all(); + for (const e in queryResults) { const entry: Entry = { - id: Number(result.id), - userId: Number(result.userId), - timestamp: Number(result.timestamp), - lastEditedTimestamp: Number(result.lastEditedTimestamp), - situation: result.situation?.toString() || "", - automaticThoughts: result.automaticThoughts?.toString() || "", + id: Number(queryResults[e].id!), + userId: Number(queryResults[e].userId!), + timestamp: Number(queryResults[e].timestamp!), + lastEditedTimestamp: Number(queryResults[e].lastEditedTimestamp!), + situation: queryResults[e].situation?.toString()!, + automaticThoughts: queryResults[e].automaticThoughts?.toString()!, emotion: { - emotionName: result.emotionName?.toString() || "", - emotionEmoji: result.emotionEmoji?.toString() || "", - emotionDescription: result.emotionDescription?.toString() || "", + emotionName: queryResults[e].emotionName?.toString()!, + emotionEmoji: queryResults[e].emotionEmoji?.toString()!, + emotionDescription: queryResults[e].emotionDescription?.toString()!, }, - selfiePath: result.selfiePath?.toString() || null, + selfiePath: queryResults[e].selfiePath?.toString()!, }; entries.push(entry); } - return entries; - }); + db.close(); + } catch (err) { + console.error( + `Jotbot Error: Failed retrieving all entries for user ${userId}: ${err}`, + ); + } + return entries; } diff --git a/models/gad7_score.ts b/models/gad7_score.ts index f65677f..af53dd1 100644 --- a/models/gad7_score.ts +++ b/models/gad7_score.ts @@ -1,8 +1,10 @@ +import { DatabaseSync } from "node:sqlite"; import { GAD7Score } from "../types/types.ts"; import { PathLike } from "node:fs"; +import { sqlFilePath } from "../constants/paths.ts"; import { anxietySeverityStringToEnum } from "../utils/misc.ts"; -import { logger } from "../utils/logger.ts"; -import { withDB } from "../utils/dbHelper.ts"; + +const sqlPath = `${sqlFilePath}/gad_score`; /** * Insert GAD-7 score into gad_score_db table @@ -11,8 +13,17 @@ import { withDB } from "../utils/dbHelper.ts"; * @returns StatementResultingChanges */ export function insertGadScore(score: GAD7Score, dbPath: PathLike) { - return withDB(dbPath, (db) => { - const queryResult = db.prepare( + let queryResult; + try { + const db = new DatabaseSync(dbPath); + db.exec("PRAGMA foreign_keys = ON;"); + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) { + throw new Error("JotBot Error: Databaes integrety check failed!"); + } + + queryResult = db.prepare( `INSERT INTO gad_score_db (userId, timestamp, score, severity, action, impactQuestionAnswer) VALUES (?, ?, ?, ?, ?, ?);`, ).run( score.userId, @@ -24,165 +35,63 @@ export function insertGadScore(score: GAD7Score, dbPath: PathLike) { ); if (queryResult.changes === 0) { - throw new Error("Insert failed: no changes made"); - } - - logger.debug( - `GAD-7 score inserted successfully: ${JSON.stringify(queryResult)}`, - ); - return queryResult; - }); -} - -/** - * Update GAD-7 score by ID - * @param id - * @param score - * @param dbPath - * @returns StatementResultingChanges - */ -export function updateGadScore( - id: number, - score: Partial, - dbPath: PathLike, -) { - return withDB(dbPath, (db) => { - const updates: string[] = []; - const values: unknown[] = []; - - if (score.score !== undefined) { - updates.push("score = ?"); - values.push(score.score); - } - if (score.severity !== undefined) { - updates.push("severity = ?"); - values.push(score.severity); - } - if (score.action !== undefined) { - updates.push("action = ?"); - values.push(score.action); - } - if (score.impactQuestionAnswer !== undefined) { - updates.push("impactQuestionAnswer = ?"); - values.push(score.impactQuestionAnswer); - } - if (score.timestamp !== undefined) { - updates.push("timestamp = ?"); - values.push(score.timestamp); - } - - if (updates.length === 0) { - throw new Error("No fields to update"); - } - - values.push(id); - const query = `UPDATE gad_score_db SET ${updates.join(", ")} WHERE id = ?;`; - - const queryResult = db.prepare(query).run(...values as number[]); - - if (queryResult.changes === 0) { - throw new Error(`Update failed: no changes made for GAD-7 score ${id}`); + throw new Error("The query ran but no changes were detected."); } - return queryResult; - }); + db.close(); + } catch (err) { + console.error(`Failed to insert gad-7 score: ${err}`); + throw new Error(`Failed to insert GAD-7 score: ${err}`); + } + console.log(queryResult); + return queryResult; } -/** - * Delete GAD-7 score by ID - * @param id - * @param dbPath - * @returns StatementResultingChanges - */ -export function deleteGadScore(id: number, dbPath: PathLike) { - return withDB(dbPath, (db) => { - const queryResult = db.prepare(`DELETE FROM gad_score_db WHERE id = ?;`) - .run(id); - - if (queryResult.changes === 0) { - logger.warn(`No GAD-7 score found with ID ${id} to delete`); - } +// export function updateGadScore(id: number) { +// // TODO +// } - return queryResult; - }); -} +// export function deleteGadScore(id: number) { +// // TODO +// } /** * @param id * @param dbPath * @returns */ -export function getGadScoreById( - id: number, - dbPath: PathLike, -): GAD7Score | undefined { - return withDB(dbPath, (db) => { - const gadScore = db.prepare(`SELECT * FROM gad_score_db WHERE id = ?;`).get( - id, - ); - if (!gadScore) return undefined; - - logger.debug(`Retrieved GAD-7 score: ${JSON.stringify(gadScore)}`); +export function getGadScoreById(id: number, dbPath: PathLike): GAD7Score { + let gadScore; + try { + const db = new DatabaseSync(dbPath); + const query = Deno.readTextFileSync(`${sqlPath}/get_gad_score_by_id.sql`) + .replace("", id.toString()).trim(); + db.exec("PRAGMA foreign_keys = ON;"); + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) { + throw new Error("JotBot Error: Databaes integrety check failed!"); + } - const gadScoreData = gadScore as { - id: number; - userId: number; - timestamp: number; - score: number; - severity: string | null; - action: string | null; - impactQuestionAnswer: string | null; - }; - return { - id: Number(gadScoreData.id), - userId: Number(gadScoreData.userId), - timestamp: Number(gadScoreData.timestamp), - score: Number(gadScoreData.score), - severity: anxietySeverityStringToEnum( - gadScoreData.severity?.toString() ?? "", - ), - action: gadScoreData.action?.toString() ?? "", - impactQuestionAnswer: gadScoreData.impactQuestionAnswer?.toString() ?? "", - }; - }); + gadScore = db.prepare(query).get(); + console.log(gadScore); + + db.close(); + } catch (err) { + console.error(`Failed to insert gad-7 score: ${err}`); + throw new Error(`Failed to insert GAD-7 score: ${err}`); + } + return { + id: Number(gadScore?.id), + userId: Number(gadScore?.userId), + timestamp: Number(gadScore?.timestamp), + score: Number(gadScore?.score), + severity: anxietySeverityStringToEnum(gadScore?.severity?.toString()!), + action: gadScore?.action?.toString()!, + impactQuestionAnswer: gadScore?.impactQuestionAnswer?.toString()!, + }; } -/** - * Get all GAD-7 scores for a user - * @param userId - * @param dbPath - * @returns - */ -export function getAllGadScoresByUserId( - userId: number, - dbPath: PathLike, -): GAD7Score[] { - return withDB(dbPath, (db) => { - const scores = db.prepare( - `SELECT * FROM gad_score_db WHERE userId = ? ORDER BY timestamp DESC;`, - ).all(userId); - - return scores.map((score) => { - const scoreData = score as { - id: number; - userId: number; - timestamp: number; - score: number; - severity: string | null; - action: string | null; - impactQuestionAnswer: string | null; - }; - return { - id: Number(scoreData.id), - userId: Number(scoreData.userId), - timestamp: Number(scoreData.timestamp), - score: Number(scoreData.score), - severity: anxietySeverityStringToEnum( - scoreData.severity?.toString() ?? "", - ), - action: scoreData.action?.toString() ?? "", - impactQuestionAnswer: scoreData.impactQuestionAnswer?.toString() ?? "", - }; - }); - }); -} +// export function getAllGadScoresByUserId(userId: number) { +// // TODO +// } diff --git a/models/journal.ts b/models/journal.ts index 04bc8c7..b151b78 100644 --- a/models/journal.ts +++ b/models/journal.ts @@ -1,22 +1,27 @@ import { PathLike } from "node:fs"; import { JournalEntry } from "../types/types.ts"; +import { DatabaseSync } from "node:sqlite"; import { sqlFilePath } from "../constants/paths.ts"; -import { logger } from "../utils/logger.ts"; -import { withDB } from "../utils/dbHelper.ts"; const sqlPath = `${sqlFilePath}/journal_entry`; /** * Stores a journal entry * @param journalEntry Journal entry to store - * @param dbFile The file path pointing to DB file - * @returns StatementResultingChanges shows changes made to DB + * @param dbFile The file path pointing to the DB file + * @returns StatementResultingChanges shows changes made to the DB */ export function insertJournalEntry( journalEntry: JournalEntry, dbFile: PathLike, ) { - return withDB(dbFile, (db) => { + try { + const db = new DatabaseSync(dbFile); + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); + const queryResult = db.prepare( `INSERT INTO journal_db (userId, timestamp, lastEditedTimestamp, content, length) VALUES (?, ?, ?, ?, ?);`, ).run( @@ -27,125 +32,149 @@ export function insertJournalEntry( journalEntry.length, ); - if (queryResult.changes === 0) { - throw new Error(`Insert failed: no changes made`); - } - + db.close(); return queryResult; - }); + } catch (err) { + console.error( + `Failed to insert journal entry into journal_db: ${err}`, + ); + throw err; + } } /** - * Updates JournalEntry passed in DB + * Updates the JournalEntry passed in the DB * @param journalEntry The journal entry to update - * @param dbFile The file path pointing to DB file - * @returns StatementResultingChanges shows changes made to DB + * @param dbFile The file path pointing to the DB file + * @returns StatementResultingChanges shows changes made to the DB */ export function updateJournalEntry( journalEntry: JournalEntry, dbFile: PathLike, ) { - return withDB(dbFile, (db) => { + try { + const db = new DatabaseSync(dbFile); const query = Deno.readTextFileSync(`${sqlPath}/update_journal_entry.sql`) - .replace("", (journalEntry.id ?? 0).toString()).trim(); + .replace("", journalEntry.id!.toString()).trim(); + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); const queryResult = db.prepare(query).run( - journalEntry.lastEditedTimestamp ?? Date.now(), + journalEntry.lastEditedTimestamp!, journalEntry.content, journalEntry.length, ); - - if (queryResult.changes === 0) { - throw new Error(`Update failed: no changes made`); - } - + db.close(); return queryResult; - }); + } catch (err) { + console.error(`Failed to update journal entry ${journalEntry.id}: ${err}`); + } } /** * Deletes a journal entry by it's id - * @param id Id of journal entry to delete - * @param dbFile The file path pointing to DB file - * @returns StatementResultingChanges shows changes made to DB + * @param id Id of the journal entry to delete + * @param dbFile The file path pointing to the DB file + * @returns StatementResultingChanges shows changes made to the DB */ export function deleteJournalEntryById( id: number, dbFile: PathLike, ) { - return withDB(dbFile, (db) => { - const queryResult = db.prepare( - `DELETE FROM journal_db WHERE id = ?;`, - ).run(id); - - if (queryResult.changes === 0) { - logger.warn(`No journal entry found with ID ${id} to delete`); - } + try { + const db = new DatabaseSync(dbFile); + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); + const queryResult = db.prepare( + `DELETE FROM journal_db WHERE id = ${id};`, + ).run(); + db.close(); return queryResult; - }); + } catch (err) { + console.error(`Failed to retrieve journal entry ${id}: ${err}`); + } } /** - * Retrieve a journal entry from database by it's id - * @param id Id of entry to retrieve - * @param dbFile The file path pointing to DB file + * Retrieve a journal entry from the database by it's id + * @param id Id of the entry to retrieve + * @param dbFile The file path pointing to the DB file * @returns JournalEntry */ export function getJournalEntryById( id: number, dbFile: PathLike, -): JournalEntry | undefined { - return withDB(dbFile, (db) => { - const journalEntry = db.prepare( - `SELECT * FROM journal_db WHERE id = ?;`, - ).get(id); - - if (!journalEntry) return undefined; +) { + try { + const db = new DatabaseSync(dbFile); + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); + const journalEntry = db.prepare( + `SELECT * FROM journal_db WHERE id = ${id};`, + ).get(); + db.close(); return { - id: Number(journalEntry.id), - userId: Number(journalEntry.userId), - imagesId: Number(journalEntry.imagesId) || null, - voiceRecordingsId: Number(journalEntry.voiceRecordingsId) || null, - timestamp: Number(journalEntry.timestamp), - lastEditedTimestamp: Number(journalEntry.lastEditedTimestamp) || null, - content: String(journalEntry.content), - length: Number(journalEntry.length), + id: Number(journalEntry?.id!), + userId: Number(journalEntry?.userId!), + imagesId: Number(journalEntry?.imagesId!) || null, + voiceRecordingsId: Number(journalEntry?.voiceRecordingsId) || null, + timestamp: Number(journalEntry?.timestamp!), + lastEditedTimestamp: Number(journalEntry?.lastEditedTimestamp) || null, + content: String(journalEntry?.content!), + length: Number(journalEntry?.length!), }; - }); + } catch (err) { + console.error(`Failed to retrieve journal entry ${id}: ${err}`); + } } /** * Grab all of a user's journal entries - * @param userId The id of user who owns journal entries - * @param dbFile The file path pointing to DB file + * @param userId The id of the user who owns the journal entries + * @param dbFile The file path pointing to the DB file * @returns JournalEntry[] */ export function getAllJournalEntriesByUserId(userId: number, dbFile: PathLike) { - return withDB(dbFile, (db) => { - const journalEntriesResults = db.prepare( - `SELECT * FROM journal_db WHERE userId = ? ORDER BY timestamp DESC;`, - ).all(userId); - const journalEntries: JournalEntry[] = []; + const journalEntries: JournalEntry[] = []; + try { + const db = new DatabaseSync(dbFile); + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); + const journalEntriesResults = db.prepare( + `SELECT * FROM journal_db WHERE userId = ${userId} ORDER BY timestamp DESC;`, + ).all(); for (const je in journalEntriesResults) { const journalEntry: JournalEntry = { - id: Number(journalEntriesResults[je]?.id), - userId: Number(journalEntriesResults[je]?.userId), - timestamp: Number(journalEntriesResults[je]?.timestamp), + id: Number(journalEntriesResults[je]?.id!), + userId: Number(journalEntriesResults[je]?.userId!), + timestamp: Number(journalEntriesResults[je]?.timestamp!), lastEditedTimestamp: Number(journalEntriesResults[je]?.lastEditedTimestamp) || null, - content: String(journalEntriesResults[je]?.content), - length: Number(journalEntriesResults[je]?.length), + content: String(journalEntriesResults[je]?.content!), + length: Number(journalEntriesResults[je]?.length!), imagesId: Number(journalEntriesResults[je]?.imagesId) || null, voiceRecordingsId: Number(journalEntriesResults[je]?.voiceRecordingsId!) || null, }; - journalEntries.push(journalEntry); } - return journalEntries; - }); + db.close(); + } catch (err) { + console.error( + `Failed to retrieve entries that belong to ${userId}: ${err}`, + ); + } + return journalEntries; } diff --git a/models/journal_entry_photo.ts b/models/journal_entry_photo.ts index 618218a..02ce5cd 100644 --- a/models/journal_entry_photo.ts +++ b/models/journal_entry_photo.ts @@ -1,12 +1,18 @@ +import { DatabaseSync } from "node:sqlite"; import { JournalEntryPhoto } from "../types/types.ts"; import { PathLike } from "node:fs"; -import { withDB } from "../utils/dbHelper.ts"; export function insertJournalEntryPhoto( jePhoto: JournalEntryPhoto, dbFile: PathLike, ) { - return withDB(dbFile, (db) => { + try { + const db = new DatabaseSync(dbFile); + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); + const queryResult = db.prepare( `INSERT INTO photo_db (entryId, path, caption, fileSize) VALUES (?, ?, ?, ?);`, ).run( @@ -16,10 +22,12 @@ export function insertJournalEntryPhoto( jePhoto.fileSize, ); - if (queryResult.changes === 0) { - throw new Error("Insert failed: no changes made"); - } - + db.close(); return queryResult; - }); + } catch (err) { + console.error( + `Failed to insert journal entry photo into photo_db: ${err}`, + ); + throw err; + } } diff --git a/models/phq9_score.ts b/models/phq9_score.ts index 53d3dca..847c1d3 100644 --- a/models/phq9_score.ts +++ b/models/phq9_score.ts @@ -1,7 +1,7 @@ import { PathLike } from "node:fs"; import { PHQ9Score } from "../types/types.ts"; +import { DatabaseSync } from "node:sqlite"; import { depressionSeverityStringToEnum } from "../utils/misc.ts"; -import { withDB } from "../utils/dbHelper.ts"; /** * @param phqScore @@ -9,7 +9,14 @@ import { withDB } from "../utils/dbHelper.ts"; * @returns */ export function insertPhqScore(phqScore: PHQ9Score, dbFile: PathLike) { - return withDB(dbFile, (db) => { + try { + const db = new DatabaseSync(dbFile); + + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); + const queryResult = db.prepare( `INSERT INTO phq_score_db (userId, timestamp, score, severity, action, impact) VALUES (?, ?, ?, ?, ?, ?);`, ).run( @@ -21,12 +28,12 @@ export function insertPhqScore(phqScore: PHQ9Score, dbFile: PathLike) { phqScore.impactQuestionAnswer, ); - if (queryResult.changes === 0) { - throw new Error("Insert failed: no changes made"); - } - + db.close(); return queryResult; - }); + } catch (err) { + console.error(`Failed to save PHQ-9 score: ${err}`); + throw err; + } } /** @@ -35,37 +42,51 @@ export function insertPhqScore(phqScore: PHQ9Score, dbFile: PathLike) { * @returns */ export function getPhqScoreByUserId(userId: number, dbFile: PathLike) { - return withDB(dbFile, (db) => { + try { + const db = new DatabaseSync(dbFile); + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); + const phqScore = db.prepare( - `SELECT * FROM phq_score_db WHERE userId = ?;`, - ).get(userId); - if (!phqScore) return undefined; + `SELECT * FROM phq_score_db WHERE userId = ${userId};`, + ).get(); return { - id: Number(phqScore.id), - userId: Number(phqScore.userId), - timestamp: Number(phqScore.timestamp), - score: Number(phqScore.score), - severity: depressionSeverityStringToEnum(String(phqScore.severity)), - action: String(phqScore.action), - impactQuestionAnswer: String(phqScore.impact), + id: Number(phqScore?.id!), + userId: Number(phqScore?.userId!), + timestamp: Number(phqScore?.timestamp!), + score: Number(phqScore?.score!), + severity: depressionSeverityStringToEnum(String(phqScore?.severity!)), + action: String(phqScore?.action!), + impactQuestionAnswer: String(phqScore?.impact!), }; - }); + } catch (err) { + console.error(`Failed to retrieve user ${userId} PHQ-9 score: ${err}`); + } } export function getPhqScoreById(id: number, dbFile: PathLike) { - return withDB(dbFile, (db) => { + try { + const db = new DatabaseSync(dbFile); + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); + const phqScore = db.prepare( - `SELECT * FROM phq_score_db WHERE id = ?;`, - ).get(id); - if (!phqScore) return undefined; + `SELECT * FROM phq_score_db WHERE id = ${id};`, + ).get(); return { - id: Number(phqScore.id), - userId: Number(phqScore.userId), - timestamp: Number(phqScore.timestamp), - score: Number(phqScore.score), - severity: depressionSeverityStringToEnum(String(phqScore.severity)), - action: String(phqScore.action), - impactQuestionAnswer: String(phqScore.impact), + id: Number(phqScore?.id!), + userId: Number(phqScore?.userId!), + timestamp: Number(phqScore?.timestamp!), + score: Number(phqScore?.score!), + severity: depressionSeverityStringToEnum(String(phqScore?.severity!)), + action: String(phqScore?.action!), + impactQuestionAnswer: String(phqScore?.impact!), }; - }); + } catch (err) { + console.error(`Failed to retrieve PHQ-9 score ${id}: ${err}`); + } } diff --git a/models/settings.ts b/models/settings.ts index cbaa6eb..6e534ad 100644 --- a/models/settings.ts +++ b/models/settings.ts @@ -1,7 +1,6 @@ import { PathLike } from "node:fs"; import { Settings } from "../types/types.ts"; -import { withDB } from "../utils/dbHelper.ts"; -import { logger } from "../utils/logger.ts"; +import { DatabaseSync } from "node:sqlite"; /** * @param userId @@ -9,22 +8,23 @@ import { logger } from "../utils/logger.ts"; * @returns */ export function insertSettings(userId: number, dbFile: PathLike) { - return withDB(dbFile, (db) => { + try { + const db = new DatabaseSync(dbFile); + + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); + const queryResult = db.prepare( `INSERT INTO settings_db (userId) VALUES (?);`, ).run(userId); - if (queryResult.changes === 0) { - logger.error( - `Failed to insert settings for user ${userId}: No changes made`, - ); - throw new Error( - `Failed to insert settings: User ID ${userId} - no changes made`, - ); - } - + db.close(); return queryResult; - }); + } catch (err) { + console.error(`Failed to insert user ${userId} settings: ${err}`); + } } export function updateSettings( @@ -32,26 +32,23 @@ export function updateSettings( updatedSettings: Settings, dbFile: PathLike, ) { - return withDB(dbFile, (db) => { + try { + const db = new DatabaseSync(dbFile); + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); + const queryResult = db.prepare( - `UPDATE OR FAIL settings_db SET storeMentalHealthInfo = ?, custom404ImagePath = ? WHERE userId = ?`, + `UPDATE OR FAIL settings_db SET storeMentalHealthInfo = ? WHERE userId = ${userId}`, ).run( Number(updatedSettings.storeMentalHealthInfo), - updatedSettings.custom404ImagePath || null, - userId, ); - - if (queryResult.changes === 0) { - logger.error( - `Failed to update settings for user ${userId}: No changes made`, - ); - throw new Error( - `Failed to update settings: User ID ${userId} - no changes made`, - ); - } - + db.close(); return queryResult; - }); + } catch (err) { + console.error(`Failed to update user ${userId} settings: ${err}`); + } } /** @@ -59,66 +56,31 @@ export function updateSettings( * @param dbFile * @returns */ -export function getSettingsById( - userId: number, - dbFile: PathLike, -): Settings | undefined { - return withDB(dbFile, (db) => { - const queryResult = db.prepare( - `SELECT * FROM settings_db WHERE userId = ?`, - ).get(userId); +export function getSettingsById(userId: number, dbFile: PathLike) { + try { + const db = new DatabaseSync(dbFile); - if (!queryResult) return undefined; + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + db.exec("PRAGMA foreign_keys = ON;"); - return { - id: Number(queryResult.id), - userId: Number(queryResult.userId), - storeMentalHealthInfo: Boolean(Number(queryResult.storeMentalHealthInfo)), - custom404ImagePath: queryResult.custom404ImagePath?.toString() || null, - }; - }); -} - -export function updateCustom404Image( - userId: number, - imagePath: string | null, - dbFile: PathLike, -) { - return withDB(dbFile, (db) => { - const existingSettings = db.prepare( - `SELECT id FROM settings_db WHERE userId = ?`, - ).get(userId); - - if (!existingSettings) { - logger.debug(`Creating new settings record for user ${userId}`); - const insertResult = db.prepare( - `INSERT INTO settings_db (userId, custom404ImagePath) VALUES (?, ?)`, - ).run(userId, imagePath); - if (insertResult.changes === 0) { - logger.error( - `Failed to insert custom 404 image settings for user ${userId}: No changes made`, - ); - throw new Error( - `Failed to insert settings: User ID ${userId} - no changes made`, - ); - } - return insertResult; - } - - logger.debug(`Updating custom 404 image for user ${userId}`); const queryResult = db.prepare( - `UPDATE settings_db SET custom404ImagePath = ? WHERE userId = ?`, - ).run(imagePath, userId); + `SELECT * FROM settings_db WHERE userId = ${userId}`, + ).get(); - if (queryResult.changes === 0) { - logger.error( - `Failed to update custom 404 image for user ${userId}: No changes made`, - ); - throw new Error( - `Failed to update settings: User ID ${userId} - no changes made`, - ); - } - - return queryResult; - }); + db.close(); + return { + id: Number(queryResult?.id!), + userId: Number(queryResult?.userId!), + storeMentalHealthInfo: Boolean( + Number(queryResult?.storeMentalHealthInfo!), + ), + selfieDirectory: String(queryResult?.selfieDirectory!), + }; + } catch (err) { + console.error( + `Failed to retrieve user ${userId} settings from ${dbFile}: ${err}`, + ); + } } diff --git a/models/user.ts b/models/user.ts index 2a58315..d2c9765 100644 --- a/models/user.ts +++ b/models/user.ts @@ -1,7 +1,6 @@ +import { DatabaseSync } from "node:sqlite"; import { User } from "../types/types.ts"; import { PathLike } from "node:fs"; -import { withDB } from "../utils/dbHelper.ts"; -import { logger } from "../utils/logger.ts"; /** * @param user @@ -9,22 +8,29 @@ import { logger } from "../utils/logger.ts"; * @returns */ export function insertUser(user: User, dbPath: PathLike) { - return withDB(dbPath, (db) => { - const queryResult = db.prepare( - `INSERT INTO user_db (telegramId, username, dob, joinedDate) VALUES (?, ?, ?, ?);`, - ).run( + try { + const db = new DatabaseSync(dbPath); + db.exec("PRAGMA foreign_keys = ON;"); + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + + const queryResult = db.prepare(` + INSERT INTO user_db (telegramId, username, dob, joinedDate) VALUES (?, ?, ?, ?); + `).run( user.telegramId, user.username, user.dob.getTime(), user.joinedDate.getTime(), ); - if (queryResult.changes === 0) { - throw new Error(`Insert failed: no changes made`); - } - + db.close(); return queryResult; - }); + } catch (err) { + console.error( + `Failed to insert user: ${user.username} into database: ${err}`, + ); + } } /** @@ -32,17 +38,22 @@ export function insertUser(user: User, dbPath: PathLike) { * @param dbFile */ export function deleteUser(userTelegramId: number, dbFile: PathLike) { - return withDB(dbFile, (db) => { - const queryResult = db.prepare( - `DELETE FROM user_db WHERE telegramId = ?;`, - ).run(userTelegramId); + try { + const db = new DatabaseSync(dbFile); + db.exec("PRAGMA foreign_keys = ON;"); + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); - if (queryResult.changes === 0) { - logger.warn(`No user found with ID ${userTelegramId} to delete`); - } + db.prepare(`DELETE FROM user_db WHERE telegramId = ${userTelegramId};`) + .run(); - return queryResult; - }); + db.close(); + } catch (err) { + console.error( + `Failed to delete user ${userTelegramId} from database: ${err}`, + ); + } } /** @@ -51,10 +62,29 @@ export function deleteUser(userTelegramId: number, dbFile: PathLike) { * @returns */ export function userExists(userTelegramId: number, dbFile: PathLike): boolean { - return withDB(dbFile, (db) => { - const result = db.prepare( - `SELECT COUNT(*) as count FROM user_db WHERE telegramId = ?`, - ).get(userTelegramId) as { count: number }; - return result.count > 0; - }); + let ue: number = 0; + try { + const db = new DatabaseSync(dbFile); + db.exec("PRAGMA foreign_keys = ON;"); + if ( + !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") + ) throw new Error("JotBot Error: Databaes integrety check failed!"); + + const user = db.prepare( + `SELECT EXISTS(SELECT 1 FROM user_db WHERE telegramId = '${userTelegramId}')`, + ).get(); + for (const u in user) { + ue = Number(user[u]); + } + db.close(); + } catch (err) { + console.error( + `Failed to check if user ${userTelegramId} exists in database: ${err}`, + ); + } + + if (ue === 1) { + return true; + } + return false; } diff --git a/tests/dbutils_test.ts b/tests/dbutils_test.ts index e7deb5f..7e117e2 100644 --- a/tests/dbutils_test.ts +++ b/tests/dbutils_test.ts @@ -3,7 +3,7 @@ import { testDbFile } from "../constants/paths.ts"; import { createUserTable } from "../db/migration.ts"; import { insertUser } from "../models/user.ts"; import { User } from "../types/types.ts"; -import { createDatabase, getLatestId } from "../utils/dbUtils.ts"; +import { getLatestId } from "../utils/dbUtils.ts"; Deno.test("Test getLatestId()", () => { const testUser: User = { @@ -17,28 +17,3 @@ Deno.test("Test getLatestId()", () => { assertEquals(getLatestId(testDbFile, "user_db"), 1); Deno.removeSync(testDbFile); }); - -Deno.test("Test createDatabase()", () => { - createDatabase(testDbFile); - - // Check that all tables exist - const tables = [ - "user_db", - "gad_score_db", - "phq_score_db", - "entry_db", - "settings_db", - "journal_db", - "journal_entry_photos_db", - "voice_recording_db", - ]; - for (const _table of tables) { - // This would throw if table doesn't exist - // We can't easily test without opening DB, but since no error, assume OK - } - - // Check that custom404ImagePath column exists in settings_db - // (This is tested in migration_test.ts, but good to verify in full createDatabase) - - Deno.removeSync(testDbFile); -}); diff --git a/tests/entry_test.ts b/tests/entry_test.ts index 467c981..b08234f 100644 --- a/tests/entry_test.ts +++ b/tests/entry_test.ts @@ -2,7 +2,6 @@ import { assertEquals } from "@std/assert/equals"; import { createEntryTable, createUserTable } from "../db/migration.ts"; import { insertUser } from "../models/user.ts"; import { Entry, User } from "../types/types.ts"; -import { logger } from "../utils/logger.ts"; import { deleteEntryById, getAllEntriesByUserId, @@ -46,7 +45,7 @@ Deno.test("Test insertEntry()", () => { // Insert test user insertUser(testUser, testDbFile); } catch (_err) { - logger.debug("User already inserted"); + console.log("User already inserted"); } // Insert test entry @@ -111,7 +110,6 @@ Deno.test("Test getEntryById()", () => { // Get entry by id const entry = getEntryById(1, testDbFile); - if (!entry) throw new Error("Expected entry to be defined"); assertObjectMatch(testEntry, entry); diff --git a/tests/gad7_score_test.ts b/tests/gad7_score_test.ts index 17b26b0..4a17564 100644 --- a/tests/gad7_score_test.ts +++ b/tests/gad7_score_test.ts @@ -43,7 +43,6 @@ Deno.test("Test getGadScoreById()", () => { insertGadScore(testGadScore, testDbFile); const gadScore = getGadScoreById(1, testDbFile); - if (!gadScore) throw new Error("Expected gadScore to be defined"); assertObjectMatch(gadScore, testGadScore); Deno.removeSync(testDbFile); }); diff --git a/tests/journal_test.ts b/tests/journal_test.ts index b3545ec..0389af1 100644 --- a/tests/journal_test.ts +++ b/tests/journal_test.ts @@ -1,7 +1,6 @@ import { assertEquals } from "@std/assert/equals"; import { testDbFile } from "../constants/paths.ts"; import { createJournalTable, createUserTable } from "../db/migration.ts"; -import { logger } from "../utils/logger.ts"; import { deleteJournalEntryById, getAllJournalEntriesByUserId, @@ -70,7 +69,7 @@ Deno.test("Test updateJournalEntry()", () => { const queryResult = updateJournalEntry(updatedJournalEntry, testDbFile); assertEquals(queryResult?.changes, 1); - logger.debug(`Update result: ${JSON.stringify(queryResult)}`); + console.log(queryResult); Deno.removeSync(testDbFile); }); diff --git a/tests/migration_test.ts b/tests/migration_test.ts index a84663d..daf668f 100644 --- a/tests/migration_test.ts +++ b/tests/migration_test.ts @@ -1,6 +1,5 @@ import { DatabaseSync } from "node:sqlite"; import { - addCustom404Column, createEntryTable, createGadScoreTable, createJournalEntryPhotosTable, @@ -132,21 +131,3 @@ Deno.test("Test createVoiceRecordingTable()", () => { assertEquals(table?.name, "voice_recording_db"); Deno.removeSync(testDbPath); }); - -Deno.test("Test addCustom404Column()", () => { - // First create the settings table - createSettingsTable(testDbPath); - - // Add the column - addCustom404Column(testDbPath); - - // Verify the column exists - const db = new DatabaseSync(testDbPath); - const columns = db.prepare("PRAGMA table_info(settings_db);").all() as { - name: string; - }[]; - const hasColumn = columns.some((col) => col.name === "custom404ImagePath"); - - assertEquals(hasColumn, true); - Deno.removeSync(testDbPath); -}); diff --git a/tests/settings_test.ts b/tests/settings_test.ts index 5357525..82abc79 100644 --- a/tests/settings_test.ts +++ b/tests/settings_test.ts @@ -4,7 +4,6 @@ import { createSettingsTable, createUserTable } from "../db/migration.ts"; import { getSettingsById, insertSettings, - updateCustom404Image, updateSettings, } from "../models/settings.ts"; import { insertUser } from "../models/user.ts"; @@ -23,7 +22,6 @@ const testUser: User = { const testSettings: Settings = { userId: 12345, storeMentalHealthInfo: false, - custom404ImagePath: null, }; Deno.test("Test insertSettings()", async () => { @@ -72,24 +70,3 @@ Deno.test("Test updateSettings()", async () => { await Deno.removeSync(testDbFile); }); - -Deno.test("Test updateCustom404Image()", async () => { - await createUserTable(testDbFile); - await createSettingsTable(testDbFile); - insertUser(testUser, testDbFile); - insertSettings(testUser.telegramId, testDbFile); - - const customPath = "assets/404/12345_404.jpg"; - const queryResult = updateCustom404Image( - testUser.telegramId, - customPath, - testDbFile, - ); - - assertEquals(queryResult?.changes, 1); - - const updatedSettings = getSettingsById(testUser.telegramId, testDbFile); - assertEquals(updatedSettings?.custom404ImagePath, customPath); - - await Deno.removeSync(testDbFile); -}); diff --git a/types/types.ts b/types/types.ts index 6ad3e77..7530dfc 100644 --- a/types/types.ts +++ b/types/types.ts @@ -68,7 +68,6 @@ export type Settings = { id?: number; userId: number; storeMentalHealthInfo: boolean; - custom404ImagePath?: string | null; }; export type JournalEntryPhoto = { diff --git a/utils/KittyEngine.ts b/utils/KittyEngine.ts index 93370bd..9234882 100644 --- a/utils/KittyEngine.ts +++ b/utils/KittyEngine.ts @@ -1,5 +1,4 @@ import { catImagesApiBaseUrl } from "../constants/strings.ts"; -import { logger } from "./logger.ts"; export class KittyEngine { baseUrl: string = catImagesApiBaseUrl; @@ -27,8 +26,8 @@ export class KittyEngine { }, ); const json = await response.json(); - logger.debug( - `Fetching cat from: ${this.baseUrl}/cat/${ + console.log( + `${this.baseUrl}/cat/${ this.tagString?.toLocaleLowerCase().replaceAll(" ", "") }`, ); diff --git a/utils/dbHelper.ts b/utils/dbHelper.ts deleted file mode 100644 index 82870c2..0000000 --- a/utils/dbHelper.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { PathLike } from "node:fs"; -import { DatabaseSync } from "node:sqlite"; - -/** - * Executes a callback with a database connection, handling common setup and cleanup. - * @param dbFile Path to the database file - * @param callback Function to execute with the database instance - * @returns The result of the callback - */ -export function withDB( - dbFile: PathLike, - callback: (db: DatabaseSync) => T, -): T { - const db = new DatabaseSync(dbFile); - try { - if ( - !(db.prepare("PRAGMA integrity_check;").get()?.integrity_check === "ok") - ) { - throw new Error("Database integrity check failed!"); - } - db.exec("PRAGMA foreign_keys = ON;"); - return callback(db); - } finally { - db.close(); - } -} diff --git a/utils/dbUtils.ts b/utils/dbUtils.ts index aa3046e..1553f82 100644 --- a/utils/dbUtils.ts +++ b/utils/dbUtils.ts @@ -1,39 +1,25 @@ +import { PathLike } from "node:fs"; import { - addCustom404Column, createEntryTable, createGadScoreTable, - createJournalEntryPhotosTable, - createJournalTable, createPhqScoreTable, createSettingsTable, createUserTable, - createVoiceRecordingTable, } from "../db/migration.ts"; import { DatabaseSync } from "node:sqlite"; -import { PathLike } from "node:fs"; -import { logger } from "./logger.ts"; /** * @param dbFile */ export function createDatabase(dbFile: PathLike) { try { - // Create an empty database file first to ensure it exists - const db = new DatabaseSync(dbFile); - db.close(); - - // Now create all tables createUserTable(dbFile); createGadScoreTable(dbFile); createPhqScoreTable(dbFile); createEntryTable(dbFile); createSettingsTable(dbFile); - createJournalTable(dbFile); - createJournalEntryPhotosTable(dbFile); - createVoiceRecordingTable(dbFile); - addCustom404Column(dbFile); // Add custom 404 column migration } catch (err) { - logger.error(`Failed to create database: ${err}`); + console.error(err); throw new Error(`Failed to create database: ${err}`); } } @@ -54,7 +40,7 @@ export function getLatestId( .replace("", tableName).trim(); id = db.prepare(query).get(); } catch (err) { - logger.error(`Failed to retrieve latest id from ${tableName}: ${err}`); + console.error(`Failed to retrieve latest id from ${tableName}: ${err}`); } - return Number(id?.max_id) || 0; + return Number(id?.seq); } diff --git a/utils/keyboards.ts b/utils/keyboards.ts index c59579f..b91eca6 100644 --- a/utils/keyboards.ts +++ b/utils/keyboards.ts @@ -39,7 +39,7 @@ export const mainKittyKeyboard: InlineKeyboard = new InlineKeyboard() .text("Inspirational 🐱", "inspiration-kitty").row() .text("Exit", "kitty-exit"); -export const questionnaireKeyboard: InlineKeyboard = new InlineKeyboard() +export const questionaireKeyboard: InlineKeyboard = new InlineKeyboard() .text("Not at all", "not-at-all").row() .text("Several days", "several-days").row() .text("More than half the days", "more-than-half-the-days").row() @@ -52,6 +52,5 @@ export const keyboardFinal: InlineKeyboard = new InlineKeyboard() .text("Extremely difficult"); export const settingsKeyboard: InlineKeyboard = new InlineKeyboard() - .text("📊 Save Mental Health Scores", "smhs").row() - .text("🖼️ Set Custom 404 Image", "set-404-image").row() - .text("⬅️ Back", "settings-back"); + .text("Save Mental Health Scores", "smhs").row() + .text("Back", "settings-back"); diff --git a/utils/logger.ts b/utils/logger.ts deleted file mode 100644 index 936f588..0000000 --- a/utils/logger.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ConsoleHandler, getLogger, type LevelName, setup } from "@std/log"; - -const LOG_LEVEL: LevelName = ( - Deno.env.get("LOG_LEVEL") || "INFO" -) as LevelName; - -setup({ - handlers: { - console: new ConsoleHandler(LOG_LEVEL), - }, - loggers: { - default: { - level: LOG_LEVEL, - handlers: ["console"], - }, - }, -}); - -export const logger = getLogger("JotBot"); diff --git a/utils/misc.ts b/utils/misc.ts index 77a4196..d8ff220 100644 --- a/utils/misc.ts +++ b/utils/misc.ts @@ -9,10 +9,9 @@ import { import { anxietyExplanations, depressionExplanations, - getTelegramDownloadUrl, + telegramDownloadUrl, } from "../constants/strings.ts"; import { File } from "grammy/types"; -import { logger } from "./logger.ts"; export function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -37,7 +36,7 @@ export function entryFromString(entryString: string): Entry { const emotionArr = emotion!.split(" "); const emotionName = emotionArr[0], emotionEmoji = emotionArr[1]; - logger.debug(`Parsed emotion array: ${JSON.stringify(emotionArr)}`); + console.log(emotionArr); return { userId: 0, @@ -56,17 +55,45 @@ export function entryFromString(entryString: string): Entry { } } +// export async function dropOrphanedSelfies() { +// const entries = getAllEntriesByUserId(); +// const selfiePaths: string[] = []; +// for (const entry in entries) { +// if (!entries[entry].selfiePath) continue; +// selfiePaths.push(entries[entry].selfiePath!); +// } + +// const dateTimes: string[][] = []; +// for (const path in selfiePaths) { +// const date = selfiePaths[path].split("_")[1]; +// const time = selfiePaths[path].split("_")[2]; +// const dateTime = []; +// dateTime.push(date, time); +// dateTimes.push(dateTime); +// } + +// const dateTimeStrings = []; +// for (const dateTime in dateTimes) { +// dateTimeStrings.push(new RegExp(dateTimes[dateTime].join("_"))); +// } + +// for await (const dirEntry of Deno.readDir("assets/selfies")) { +// for (const regex in dateTimeStrings) { +// if (!dateTimeStrings[regex].test(dirEntry.name)) { +// Deno.removeSync(`assets/selfies/${dirEntry.name}`); +// } +// } +// } +// } + export function entryToString(entry: Entry): string { let lastEditedString: string = ""; - if ( - entry.lastEditedTimestamp !== undefined && - entry.lastEditedTimestamp !== null - ) { + if (entry.lastEditedTimestamp !== undefined) { lastEditedString = `Last Edited ${ - new Date(entry.lastEditedTimestamp).toLocaleString() + new Date(entry.lastEditedTimestamp!).toLocaleString() }`; } - return `Date Created ${new Date(entry.timestamp).toLocaleString()} + return `Date Created ${new Date(entry.timestamp!).toLocaleString()} ${lastEditedString} Emotion ${entry.emotion.emotionName} ${entry.emotion.emotionEmoji || ""} @@ -105,7 +132,7 @@ export function calcPhq9Score( depressionSeverity = DepressionSeverity.SEVERE; depressionExplanation = depressionExplanations.severe; } else { - logger.error("Depression Score out of bounds!"); + console.log("Depression Score out of bounds!"); } return { @@ -139,7 +166,7 @@ export function calcGad7Score( anxietySeverity = AnxietySeverity.MODERATE_TO_SEVERE_ANXIETY; anxietyExplanation = anxietyExplanations.severe_anxiety; } else { - logger.error("Anxiety Score out of bounds!"); + console.log("Depression Score out of bounds!"); } return { @@ -220,7 +247,6 @@ export async function downloadTelegramImage( caption: string, telegramFile: File, journalEntryId: number, - apiBaseUrl: string = "https://api.telegram.org", ): Promise { const journalEntryPhoto: JournalEntryPhoto = { journalEntryId: journalEntryId, @@ -229,14 +255,14 @@ export async function downloadTelegramImage( fileSize: 0, }; try { - if (!telegramFile.file_path) { - throw new Error("Telegram file path is missing"); - } const selfieResponse = await fetch( - getTelegramDownloadUrl(apiBaseUrl, token, telegramFile.file_path), + telegramDownloadUrl.replace("", token).replace( + "", + telegramFile.file_path!, + ), ); - journalEntryPhoto.fileSize = telegramFile.file_size ?? 0; + journalEntryPhoto.fileSize = telegramFile.file_size!; journalEntryPhoto.caption = caption; if (selfieResponse.body) { @@ -252,9 +278,9 @@ export async function downloadTelegramImage( journalEntryPhoto.path = filePath; - logger.debug(`Saving file: ${filePath}`); + console.log(`File: ${file}`); journalEntryPhoto.path = await Deno.realPath(filePath); - await selfieResponse.body.pipeTo(file.writable); + await selfieResponse.body!.pipeTo(file.writable); } } catch (err) { throw err; diff --git a/utils/retry.ts b/utils/retry.ts deleted file mode 100644 index bea7b8b..0000000 --- a/utils/retry.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { logger } from "./logger.ts"; - -/** - * Retry utility for operations that might fail intermittently - */ - -export interface RetryOptions { - maxAttempts?: number; - baseDelay?: number; - maxDelay?: number; - backoffFactor?: number; - retryCondition?: (error: unknown) => boolean; -} - -export class RetryError extends Error { - constructor( - message: string, - public readonly attempts: number, - public readonly lastError: unknown, - ) { - super(message); - this.name = "RetryError"; - } -} - -/** - * Executes a function with automatic retry logic - */ -export async function withRetry( - operation: () => Promise, - options: RetryOptions = {}, -): Promise { - const { - maxAttempts = 3, - baseDelay = 1000, - maxDelay = 30000, - backoffFactor = 2, - retryCondition = () => true, - } = options; - - let lastError: unknown; - - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - return await operation(); - } catch (error) { - lastError = error; - - // Don't retry if this is the last attempt - if (attempt === maxAttempts) { - break; - } - - // Check if we should retry this error - if (!retryCondition(error)) { - throw error; - } - - // Calculate delay with exponential backoff - const delay = Math.min( - baseDelay * Math.pow(backoffFactor, attempt - 1), - maxDelay, - ); - - logger.info( - `Operation failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - await new Promise((resolve) => setTimeout(resolve, delay)); - } - } - - throw new RetryError( - `Operation failed after ${maxAttempts} attempts`, - maxAttempts, - lastError, - ); -} - -/** - * Retry conditions for different types of errors - */ -export const retryConditions = { - network: (error: unknown) => { - // Retry on network errors, timeouts, and certain HTTP status codes - const err = error as { name?: string; code?: string; message?: string }; - if ( - err?.name === "HttpError" || err?.code === "ETIMEDOUT" || - err?.code === "ENOTFOUND" - ) { - return true; - } - if ( - err?.message?.includes("Network request") || - err?.message?.includes("timeout") - ) { - return true; - } - return false; - }, - - database: (error: unknown) => { - // Retry on database connection issues, locks, etc. - const err = error as { code?: string; message?: string }; - if (err?.code === "SQLITE_BUSY" || err?.code === "SQLITE_LOCKED") { - return true; - } - if ( - err?.message?.includes("database") || err?.message?.includes("SQLITE") - ) { - return true; - } - return false; - }, - - api: (error: unknown) => { - // Retry on API rate limits, temporary server errors - const err = error as { error_code?: number }; - if (err?.error_code === 429) { // Rate limit - return true; - } - if ( - err?.error_code !== undefined && err.error_code >= 500 && - err.error_code < 600 - ) { // Server errors - return true; - } - return retryConditions.network(error); - }, -};