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 9d9af52..0000000 Binary files a/assets/404/1680564645_404.jpg and /dev/null differ 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); - }, -};