Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TELEGRAM_BOT_KEY=your_bot_token_here
TELEGRAM_API_BASE_URL=https://api.telegram.org
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/assets/selfies/*
/assets/archive/selfies/*
/assets/journal_entry_images/*
/assets/custom_404/*
/bin
*.db
.env
jotbot
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@
Jotbot is a telegram bot that can help you to record your thoughts and emotions
in directly in the telegram app.

## Configuration

Jotbot uses environment variables for configuration. Copy `.env.example` to
`.env` and fill in your values:

```bash
cp .env.example .env
```

### Environment Variables

| Variable | Description | Default |
| ----------------------- | --------------------------------------- | -------------------------- |
| `TELEGRAM_BOT_KEY` | Your Telegram bot token from @BotFather | (required) |
| `TELEGRAM_API_BASE_URL` | Custom Telegram Bot API URL | `https://api.telegram.org` |

The `TELEGRAM_API_BASE_URL` is useful when running behind a proxy or using a
self-hosted Telegram API.

## How do I use Jotbot?

Jotbot is easy to use you have to be "registered" to start recording entries.
Expand Down
2 changes: 2 additions & 0 deletions constants/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export const dbFile: string = `${dbFileBasePath}/jotbot.db`;
export const testDbFileBasePath = `db/test_db`;
export const testDbFile: string = `${testDbFileBasePath}/jotbot_test.db`;
export const selfieDirPath: string = `${Deno.cwd()}/assets/selfies`;
export const custom404DirPath: string = `${Deno.cwd()}/assets/custom_404`;
export const sqlFilePath = "db/sql";
export const default404ImagePath = "assets/404.png";
2 changes: 0 additions & 2 deletions constants/strings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
export const startString: string =
`Hello! Welcome to JotBot. I'm here to help you record your emotions and emotion!`;
export const telegramDownloadUrl =
"https://api.telegram.org/file/bot<token>/<file_path>";

export const catImagesApiBaseUrl = `https://cataas.com`;
export const quotesApiBaseUrl = `https://zenquotes.io/api/quotes/`;
Expand Down
12 changes: 12 additions & 0 deletions db/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ export function createSettingsTable(dbFile: PathLike) {
}
}

export function addCustom404ImagePathColumn(dbFile: PathLike) {
try {
const db = new DatabaseSync(dbFile);
db.exec("PRAGMA foreign_keys = ON;");
db.prepare("ALTER TABLE settings_db ADD COLUMN custom404ImagePath TEXT;")
.run();
db.close();
} catch (err) {
console.error(`Failed to add custom404ImagePath column: ${err}`);
}
}

export function createJournalTable(dbFile: PathLike) {
try {
const db = new DatabaseSync(dbFile);
Expand Down
1 change: 1 addition & 0 deletions db/sql/create_settings_table.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ CREATE TABLE IF NOT EXISTS settings_db (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER,
storeMentalHealthInfo INTEGER DEFAULT 0,
custom404ImagePath TEXT,
FOREIGN KEY (userId) REFERENCES user_db(telegramId) ON DELETE CASCADE
);
9 changes: 9 additions & 0 deletions handlers/delete_account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Context } from "grammy";
import { Conversation } from "@grammyjs/conversations";
import { deleteAccountConfirmKeyboard } from "../utils/keyboards.ts";
import { deleteUser } from "../models/user.ts";
import { getSettingsById } from "../models/settings.ts";
import { dbFile } from "../constants/paths.ts";

export async function delete_account(conversation: Conversation, ctx: Context) {
Expand All @@ -17,6 +18,14 @@ export async function delete_account(conversation: Conversation, ctx: Context) {
]);

if (deleteAccountCtx.callbackQuery.data === "delete-account-yes") {
const settings = getSettingsById(ctx.from?.id!, dbFile);
if (settings?.custom404ImagePath) {
try {
await Deno.remove(settings.custom404ImagePath);
} catch (err) {
console.error(`Failed to delete custom 404 image: ${err}`);
}
}
await conversation.external(() => deleteUser(ctx.from?.id!, dbFile));
} else if (deleteAccountCtx.callbackQuery.data === "delete-account-no") {
conversation.halt();
Expand Down
4 changes: 2 additions & 2 deletions handlers/new_entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Context, InlineKeyboard } from "grammy";
import { Conversation } from "@grammyjs/conversations";
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 { getTelegramDownloadUrl } from "../utils/telegram.ts";

export async function new_entry(conversation: Conversation, ctx: Context) {
// Describe situation
Expand Down Expand Up @@ -74,7 +74,7 @@ export async function new_entry(conversation: Conversation, ctx: Context) {
const tmpFile = await selfiePathCtx.getFile();
// console.log(selfiePathCtx.message.c);
const selfieResponse = await fetch(
telegramDownloadUrl.replace("<token>", ctx.api.token).replace(
getTelegramDownloadUrl().replace("<token>", ctx.api.token).replace(
"<file_path>",
tmpFile.file_path!,
),
Expand Down
43 changes: 41 additions & 2 deletions handlers/register.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { Context, InlineKeyboard } from "grammy";
import { Conversation } from "@grammyjs/conversations";
import { insertUser } from "../models/user.ts";
import { insertUser, userExists } from "../models/user.ts";
import { User } from "../types/types.ts";
import { dbFile } from "../constants/paths.ts";
import { getSettingsById, insertSettings } from "../models/settings.ts";

export async function register(conversation: Conversation, ctx: Context) {
// Check if user already exists
if (userExists(ctx.from?.id!, dbFile)) {
await ctx.reply(
`You are already registered, ${ctx.from?.username}! Use /new_entry to create a new entry.`,
);
return;
}

let dob;
try {
while (true) {
Expand Down Expand Up @@ -37,11 +46,41 @@ export async function register(conversation: Conversation, ctx: Context) {
console.log(user);
try {
insertUser(user, dbFile);
console.log("User inserted, now inserting settings...");
insertSettings(ctx.from?.id!, dbFile);
console.log("Settings inserted for user:", ctx.from?.id);
const settingsCheck = getSettingsById(ctx.from?.id!, dbFile);
console.log("Settings after insert:", settingsCheck);
} catch (err) {
ctx.reply(`Failed to save user ${user.username}: ${err}`);
console.log(`Error inserting user ${user.username}: ${err}`);
}
ctx.reply(

await ctx.editMessageText(
`Before we finish, would you like to set a custom 404 image? This image will be shown when viewing entries without a selfie.`,
{
reply_markup: new InlineKeyboard().text("Yes", "set-custom-404").text(
"No",
"skip-custom-404",
),
},
);

const custom404Ctx = await conversation.waitForCallbackQuery([
"set-custom-404",
"skip-custom-404",
]);

if (custom404Ctx.callbackQuery.data === "set-custom-404") {
await custom404Ctx.editMessageText("Setting up custom 404 image...");
await conversation.select("set_custom_404_image");
} else {
await custom404Ctx.editMessageText(
"Skipped. You can set a custom 404 image anytime from Settings.",
);
}

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") },
);
Expand Down
103 changes: 103 additions & 0 deletions handlers/set_custom_404_image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Context } from "grammy";
import { Conversation } from "@grammyjs/conversations";
import { getSettingsById, updateSettings } from "../models/settings.ts";
import { custom404DirPath, dbFile } from "../constants/paths.ts";
import { getTelegramDownloadUrl } from "../utils/telegram.ts";

export async function set_custom_404_image(
conversation: Conversation,
ctx: Context,
) {
const userId = ctx.from?.id!;
const settings = getSettingsById(userId, dbFile);
console.log("Current settings:", settings);

if (settings?.custom404ImagePath) {
await ctx.reply(
`You already have a custom 404 image set at: ${settings.custom404ImagePath}. Send a new image to replace it, /default to reset to default, or /cancel to keep current.`,
);
} else {
await ctx.reply(
`Send an image to use as your custom 404 image, /default to reset to default, or /cancel to skip.`,
);
}

const choiceCtx = await conversation.wait();

if (choiceCtx.message?.text === "/cancel") {
await ctx.reply("Cancelled. Your custom 404 image has not been changed.");
return;
}

if (choiceCtx.message?.text === "/default") {
const currentPath = settings?.custom404ImagePath;
if (currentPath) {
try {
await Deno.remove(currentPath);
console.log("Deleted old custom 404 image:", currentPath);
} catch (err) {
console.error(`Failed to delete custom 404 image: ${err}`);
}
}
settings!.custom404ImagePath = null;
await conversation.external(() =>
updateSettings(userId, settings!, dbFile)
);
console.log("Reset custom404ImagePath to null");
await ctx.reply("Reset to default 404 image.");
return;
}

if (choiceCtx.message?.photo) {
try {
const tmpFile = await choiceCtx.getFile();
console.log("Downloading file from:", tmpFile.file_path);
const selfieResponse = await fetch(
getTelegramDownloadUrl().replace("<token>", ctx.api.token).replace(
"<file_path>",
tmpFile.file_path!,
),
);

if (selfieResponse.body) {
await conversation.external(async () => {
const fileName = `404_${userId}.jpg`;
const filePath = `${custom404DirPath}/${fileName}`;
console.log("Saving custom 404 image to:", filePath);
const file = await Deno.open(filePath, {
write: true,
create: true,
});

const currentPath = settings?.custom404ImagePath;
if (currentPath && currentPath !== filePath) {
try {
Deno.removeSync(currentPath);
console.log("Deleted old custom 404 image:", currentPath);
} catch (err) {
console.error(`Failed to delete old custom 404 image: ${err}`);
}
}

const realPath = await Deno.realPath(filePath);
await selfieResponse.body.pipeTo(file.writable);
console.log("Custom 404 image saved to:", realPath);

settings!.custom404ImagePath = realPath;
updateSettings(userId, settings!, dbFile);
console.log("Updated settings with custom404ImagePath:", realPath);
});

await ctx.reply("Custom 404 image saved successfully!");
}
} catch (err) {
console.log(`Jotbot Error: Failed to save custom 404 image: ${err}`);
await ctx.reply("Failed to save custom 404 image. Please try again.");
}
return;
}

await ctx.reply(
"Invalid input. Please send an image, /default, or /cancel.",
);
}
15 changes: 11 additions & 4 deletions handlers/view_entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,25 @@ import { Entry } from "../types/types.ts";
import { viewEntriesKeyboard } from "../utils/keyboards.ts";
import { entryFromString } from "../utils/misc.ts";
import { InputFile } from "grammy/types";
import { dbFile } from "../constants/paths.ts";
import { dbFile, default404ImagePath } from "../constants/paths.ts";
import { getSettingsById } from "../models/settings.ts";

export async function view_entries(conversation: Conversation, ctx: Context) {
let entries: Entry[] = await conversation.external(() =>
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.");
}

const settings = getSettingsById(ctx.from?.id!, dbFile);
console.log("Settings for user:", settings);
const custom404Path = settings?.custom404ImagePath;
console.log("Custom 404 path:", custom404Path);
const fallback404Image = custom404Path || default404ImagePath;
console.log("Using fallback image:", fallback404Image);

let currentEntry: number = 0;
let lastEditedTimestampString = `<b>Last Edited</b> ${
entries[currentEntry].lastEditedTimestamp
Expand Down Expand Up @@ -54,7 +61,7 @@ Page <b>${currentEntry + 1}</b> of <b>${entries.length}</b>

// Reply initially with first entry before starting loop
const displaySelfieMsg = await ctx.replyWithPhoto(
new InputFile(entries[currentEntry].selfiePath! || "assets/404.png"),
new InputFile(entries[currentEntry].selfiePath! || fallback404Image),
{ caption: selfieCaptionString, parse_mode: "HTML" },
);

Expand Down Expand Up @@ -266,7 +273,7 @@ Page <b>${currentEntry + 1}</b> of <b>${entries.length}</b>
ctx.chatId!,
displaySelfieMsg.message_id,
InputMediaBuilder.photo(
new InputFile(entries[currentEntry].selfiePath! || "assets/404.png"),
new InputFile(entries[currentEntry].selfiePath! || fallback404Image),
{ caption: selfieCaptionString, parse_mode: "HTML" },
),
);
Expand Down
Loading
Loading