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
55 changes: 36 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,22 +99,28 @@ docker compose -f docker-compose.yml -f docker-compose.dev.yml down
| Variable | Required | Default | Description |
| --- | :---: | --- | --- |
| `BOT_TOKEN` | Yes | — | Telegram token issued by BotFather. |
| `POSTGRES_PASSWORD` | Recommended | `telebot` | Password for the `telebot` database user. |
| `POSTGRES_PASSWORD` | No | `telebot` | Password for the `telebot` database user. Set a strong value outside local development. |
| `BACKEND_PORT` | No | `3000` | Backend port exposed on the host. |
| `BACKEND_URL` | No | `http://backend:3000` | API URL used by the bot outside the standard Compose setup. |
| `BACKEND_URL` | No | `http://localhost:3000` outside Compose | API URL used by the bot. Both Compose files set it to `http://backend:3000`. |
| `ADMIN_PANEL_PORT` | No | `3001` | Admin panel port exposed on the host. |
| `ADMINER_PORT` | No | `8080` | Adminer port exposed on the host. |
| `VITE_BACKEND_URL` | No | `http://localhost:3000` | Browser-visible API URL baked into the admin build. |
| `ALLOWED_USERS` | No | Empty | Comma-separated Telegram user IDs allowed by backend rules. |
| `ADMIN_USERS` | No | Empty | Comma-separated Telegram user IDs with admin status. |
| `CHAT_ID` | No | — | Legacy fallback Telegram chat ID. |
| `BILLS_THREAD_ID` | No | — | Legacy fallback topic for new bills. |
| `PAID_THREAD_ID` | No | — | Legacy fallback topic for payment confirmations. |
| `VITE_BACKEND_URL` | No | `http://localhost:3000` in development | Browser-visible API URL compiled into a development admin build. |
| `ALLOWED_USERS` | No | Empty (allow all new users) | Comma-separated Telegram IDs used to set `is_allowed` when a user is first registered. |
| `ADMIN_USERS` | No | Empty | Comma-separated Telegram IDs used to set `is_admin` when a user is first registered. |
| `CHAT_ID` | No | — | Legacy chat ID consumed by the unused `constant_payment` helper. |
| `BILLS_THREAD_ID` | No | — | Fallback topic for new bills when the selected installation has no configured topic. |
| `PAID_THREAD_ID` | No | — | Fallback topic for payment confirmations when the selected installation has no configured topic. |
| `LOG` | No | `false` | Enables additional logging when set to `true`. |

`VITE_BACKEND_URL` is used by the visitor's browser, so it must be reachable
from that browser. A Docker-internal hostname such as `backend` will not work
for users outside the Compose network.
for users outside the Compose network. Vite replaces this value at build time,
not when the container starts. The development Compose overlay supplies it to
the Vite build; the prebuilt production image uses the value with which that
image was published.

`ALLOWED_USERS` and `ADMIN_USERS` do not overwrite existing database records on
restart. Change an existing user's flags from the admin panel or API.

## Bot Commands

Expand All @@ -128,6 +134,7 @@ for users outside the Compose network.
| `/delete` | Delete a registered bill. |
| `/cancel` | Cancel the current operation. |
| `/help` | Show the available commands. |
| `/ia` | Display the bot's informational AI placeholder message. |

To create a bill, a user must first be seen in a group or channel where the bot
is present. The flow then asks for the target chat, bill details, amount, and
Expand Down Expand Up @@ -276,9 +283,11 @@ The schema is defined in `backend/src/db/schema.sql` and contains:
- `bot_installation_topics`
- `bot_installation_users`

The backend applies the schema during startup. The development setup stores
PostgreSQL data in the `postgres_data` Docker volume. To discard all local
development data and rebuild the database, run:
The backend applies the idempotent schema during every startup. PostgreSQL also
runs the mounted schema automatically when it initializes an empty data
directory. The development setup stores PostgreSQL data in the `postgres_data`
Docker volume. To discard all local development data and rebuild the database,
run:

```bash
docker compose -f docker-compose.yml -f docker-compose.dev.yml down -v
Expand All @@ -293,33 +302,36 @@ docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build

## Run Services Without Docker

Use Node.js 20 or newer and start PostgreSQL separately. Set `DATABASE_URL` for
the backend and use separate terminals for each service.
Use Node.js 20.19 or newer (or Node.js 22.12 or newer) and start PostgreSQL
separately. Set `DATABASE_URL` for the backend and use separate terminals for
each service. Run `npm ci` in each directory for a reproducible install.

```bash
# Backend
cd backend
npm install
npm ci
DATABASE_URL=postgres://telebot:password@localhost:5432/telebot npm run dev
```

```bash
# Telegram bot
cd tele_bot
npm install
npm ci
BOT_TOKEN=your_token BACKEND_URL=http://localhost:3000 npm run dev
```

```bash
# Admin panel
cd admin_panel
npm install
npm ci
VITE_BACKEND_URL=http://localhost:3000 npm run dev
```

## Production
## Deployment with Compose

The production Compose file uses images published to GitHub Container Registry:
The base Compose file uses prebuilt images from GitHub Container Registry. The
current image tags are `:dev`, despite the services running with
`NODE_ENV=production`:

```bash
docker compose pull
Expand All @@ -328,6 +340,11 @@ docker compose ps
```

The admin image is built with Vite and served by nginx on container port `80`.
Its backend URL is fixed when the image is built; setting `VITE_BACKEND_URL` on
the running production container does not change it. The PostgreSQL service
bind-mounts `/Kojo/Docker/tele_bot_node/postgres`, so adjust that host path in
`docker-compose.yml` before deploying on a different machine.

To inspect a service, use `docker compose logs -f backend`, `tele_bot`, or
`admin_panel`.

Expand Down
52 changes: 51 additions & 1 deletion backend/src/repositories/agenda.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ function uniqueByTelegramId(users) {
return uniqueUsers;
}

/**
* @param {Object} data - The data to resolve the responsible users.
* @return {Array|null} The responsible users or null.
*/
function explicitResponsibleUsers(data) {
const explicit = data.responsibleUserIds || data.responsibleTelegramIds ||
data.members;
Expand All @@ -65,6 +69,10 @@ function explicitResponsibleUsers(data) {
return null;
}

/**
* @param {number|string} value - The value to parse as money.
* @return {number} The parsed money.
*/
function parseMoney(value) {
if (typeof value === "number") {
return value;
Expand All @@ -79,6 +87,10 @@ function parseMoney(value) {
return Number.isFinite(amount) ? amount : 0;
}

/**
* @param {number|string} value - The value to parse as due date.
* @return {string} The parsed due date.
*/
function parseDueDate(value) {
const text = String(value ?? "");
const brazilianDate = text.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
Expand All @@ -90,6 +102,10 @@ function parseDueDate(value) {
return text;
}

/**
* @param {Object} row - The row to map the agenda.
* @return {Object} The mapped agenda.
*/
function mapAgenda(row) {
if (!row) {
return row;
Expand Down Expand Up @@ -123,6 +139,11 @@ function mapAgenda(row) {
};
}

/**
* @param {Object} data - The data to build the update.
* @param {Object} columnMap - The column map.
* @return {Object|null} The build update.
*/
function buildUpdate(data, columnMap) {
const entries = Object.entries(data)
.filter(([field]) =>
Expand Down Expand Up @@ -154,6 +175,12 @@ function buildUpdate(data, columnMap) {
};
}

/**
* @param {Object} db - The database client.
* @param {Object} data - The data to resolve the responsible users.
* @param {Object} creator - The creator user.
* @return {Promise<Array>} The resolved responsible users.
*/
async function resolveResponsibleUsers(db, data, creator) {
const explicitUsers = explicitResponsibleUsers(data);
if (explicitUsers) {
Expand Down Expand Up @@ -203,6 +230,12 @@ async function resolveResponsibleUsers(db, data, creator) {
return creator ? [creator] : [];
}

/**
* @param {Object} db - The database client.
* @param {number} telegramUserId - The Telegram user id.
* @param {number} chatId - The chat id.
* @return {Promise<boolean>} True if the user belongs to the chat, false otherwise.
*/
async function userBelongsToBotInstallationChat(db, telegramUserId, chatId) {
const result = await db.query(
`SELECT 1
Expand All @@ -220,6 +253,12 @@ async function userBelongsToBotInstallationChat(db, telegramUserId, chatId) {
return result.rowCount > 0;
}

/**
* @param {Object} db - The database client.
* @param {number} telegramChatId - The Telegram chat id.
* @param {Array} users - The users to filter.
* @return {Promise<Array>} The filtered users.
*/
async function filterUsersByBotInstallationChatId(db, telegramChatId, users) {
const telegramIds = uniqueByTelegramId(users).map((user) =>
Number(user.telegram_id)
Expand Down Expand Up @@ -248,6 +287,11 @@ async function filterUsersByBotInstallationChatId(db, telegramChatId, users) {
return result.rows;
}

/**
* @param {Object} db - The database client.
* @param {Object} data - The data to resolve the bank.
* @return {Promise<Object|null>} The resolved bank or null.
*/
async function resolveBank(db, data) {
if (data.bankId) {
const result = await db.query(
Expand All @@ -274,6 +318,11 @@ async function resolveBank(db, data) {
return null;
}

/**
* @param {Object} db - The database client.
* @param {number} id - The id of the agenda.
* @return {Promise<Object|null>} The agenda row or null.
*/
async function getAgendaRow(db, id) {
const result = await db.query(
`SELECT
Expand Down Expand Up @@ -317,6 +366,7 @@ async function getAgendaRow(db, id) {
* Inserts a new agenda payment.
* @param {Object} data - The data to insert.
* @param {Function} next - Callback called with the inserted agenda or false.
* @return {Promise<Object|boolean>} The inserted agenda or false.
*/
async function insetAgendaPayment(data, next) {
const db = await getClient();
Expand Down Expand Up @@ -441,7 +491,7 @@ async function insetAgendaPayment(data, next) {
/**
* Retrieves unpaid agenda payments by responsible Telegram user.
* @param {number} senderId - The Telegram user id to search for.
* @return {Promise<Array|boolean>} Matching agenda payments or false.
* @return {Promise<Array|{ error: string, status: number }>} The matching agenda payments or false.
*/
async function getAllAgendaPaymentBySender(senderId) {
try {
Expand Down
7 changes: 2 additions & 5 deletions backend/src/repositories/users.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@ import { getClient, query } from "../db/postgres.js";

function telegramDisplayName(user = {}) {
return user.displayName ||
user.display_name ||
[user.firstName || user.first_name, user.lastName || user.last_name].filter(
Boolean,
).join(" ") ||
[user.firstName, user.lastName].filter(Boolean).join(" ") ||
user.username ||
String(user.telegramId || user.telegram_id || user.senderId || "");
String(user.firstName || "");
}

function userPayloadFromData(data = {}) {
Expand Down
25 changes: 25 additions & 0 deletions backend/src/services/agenda.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,55 @@ import {
updateAgendaPayment,
} from "../repositories/agenda.repository.js";

/**
* @param {Object} data - The data to create the agenda payment.
* @return {Promise<Object>} The created agenda payment.
*/
function createAgendaPayment(data) {
return new Promise((resolve) => {
insetAgendaPayment(data, resolve);
});
}

/**
* @return {Promise<Array>} The all agenda payments.
*/
function getAllAgendaPayments() {
return new Promise((resolve) => {
getAllAgendaPayment(resolve);
});
}

/**
* @param {number} telegramId - The Telegram user id.
* @return {Promise<Array>} The agenda payments by user.
*/
async function getAgendaPaymentsByUser(telegramId) {
return getAllAgendaPaymentBySender(telegramId);
}

/**
* @param {number} id - The id of the agenda payment.
* @return {Promise<Object>} The agenda payment by id.
*/
async function getAgendaPayment(id) {
return getAgendaPaymentById(id);
}

/**
* @param {number} id - The id of the agenda payment.
* @param {Object} data - The data to update the agenda payment.
* @param {number|null} telegramId - The Telegram user id.
* @return {Promise<Object>} The updated agenda payment.
*/
async function updateAgenda(id, data, telegramId = null) {
return updateAgendaPayment(id, data, telegramId);
}

/**
* @param {number} id - The id of the agenda payment.
* @return {Promise<Object>} The deleted agenda payment.
*/
async function deleteAgenda(id) {
return deleteAgendaPayment(id);
}
Expand Down
14 changes: 8 additions & 6 deletions tele_bot/src/agendas/agenda_payment.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import moment from "moment";
import agendaFormatter from "../utilities/agenda_formatter.js";
import getMemberName from "../utilities/get_member_name.js";
import generateBankKeyboard from "../utilities/generate_banks_keyboard.js";
import getUserPixBySenderBankAsKeyboard from "../utilities/get_all_pix_as_keyboard.js";
import {
getBotInstallation,
getBotInstallationUsers,
getUserBotInstallations,
insetAgendaPayment,
insetPix,
} from "../api/backendClient.js";
import agendaFormatter, {
getAgendaMemberDisplayName,
} from "../utilities/agenda_formatter.js";
import generateBankKeyboard from "../utilities/generate_banks_keyboard.js";
import getUserPixBySenderBankAsKeyboard from "../utilities/get_all_pix_as_keyboard.js";


function parseCurrency(value) {
if (typeof value === "number") {
Expand Down Expand Up @@ -263,7 +263,7 @@ class AgendaPayment {
}

memberLabel(member) {
return getAgendaMemberDisplayName(member);
return getMemberName(member);
}

generateMembersKeyboard() {
Expand Down Expand Up @@ -420,6 +420,8 @@ class AgendaPayment {
);
return true;
}
console.log(isInseted);

const paymentSummary = agendaFormatter(isInseted);
// Send the summary message
this.bot.sendMessage(isInseted.chatId, paymentSummary, {
Expand Down
7 changes: 2 additions & 5 deletions tele_bot/src/agendas/paid.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
// const allAgendaAsKeyboard = require("../utilities/all_agenda_as_keyboard");
// const agendaFormatter = require("../utilities/agenda_formatter");

import allAgendaAsKeyboard from "../utilities/all_agenda_as_keyboard.js";
import agendaFormatter from "../utilities/agenda_formatter.js";
import {
getBotInstallation,
updateAgendaPayment,
} from "../api/backendClient.js";
import allAgendaAsKeyboard from "../utilities/all_agenda_as_keyboard.js";
import agendaFormatter from "../utilities/agenda_formatter.js";

class paid {
constructor(bot) {
Expand Down
Loading
Loading