Files
arestools/src/business/connections.js
T
ros 0e19c84a6d Make bot multi-tenant with colorful welcome and per-owner isolation
The bot is now public: anyone connects it to their own Telegram Business
account and gets antidelete for their own private chats. On connection
(business_connection enabled) and on /start it sends a colorful welcome
describing features, how to connect, and limitations.

- connections.js: resolve a connection's owner from business_connection_id
  (memo -> DB -> getBusinessConnection). Business messages have no outgoing
  flag, so the owner's own messages are filtered by comparing from.id; if
  the owner can't be resolved the message is not cached.
- Strict per-owner isolation: captures scoped by owner_id; the panel shows
  each user only their own feed; notifications go to the owner's chat.
- db.js: multi-tenant schema (connections table; messages keyed by
  (conn_id, chat_id, msg_id); captures/counts scoped by owner_id).
- media.js: key cached-media filenames by (connId, chatId, msgId) to
  prevent one tenant overwriting another's encrypted media.
- panel.js: drop the owner-only barrier; /start sends welcome + own feed.
- config.js: OWNER_ID is now optional (service logs only, grants no access).
- Docs: README/.env.example rewritten for the multi-tenant model and the
  shared-key privacy caveat.
- Stop tracking .claude/settings.local.json; restore the Hcrgram/ ignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 22:16:44 +03:00

68 lines
2.8 KiB
JavaScript

import { getConnection, upsertConnection } from '../core/db.js';
import { logger } from '../core/logger.js';
/**
* Резолвер владельца по business_connection_id.
*
* Зачем: у бизнес-сообщений НЕТ флага «исходящее». Единственный надёжный способ
* отличить сообщение владельца от сообщения собеседника — сравнить from.id с id
* владельца подключения. А уведомления об удалении надо слать в личку владельца
* (owner_chat_id). И то, и другое берём из подключения.
*
* Порядок поиска: память → БД → getBusinessConnection (и результат кэшируем).
* getBusinessConnection нужен после рестарта, если событие connection пришло до
* запуска и в памяти пусто, но в БД запись есть — тогда чаще хватит БД.
*/
const memo = new Map(); // connId -> { ownerId, ownerChatId, ownerName, ownerUsername, isEnabled }
/** Кладёт/обновляет подключение и в память, и в БД. Возвращает нормализованную запись. */
export function rememberConnection(conn) {
const record = {
connId: String(conn.id),
ownerId: conn.user?.id != null ? String(conn.user.id) : '',
ownerChatId: conn.user_chat_id != null ? String(conn.user_chat_id) : '',
ownerName: [conn.user?.first_name, conn.user?.last_name].filter(Boolean).join(' '),
ownerUsername: conn.user?.username ?? '',
isEnabled: Boolean(conn.is_enabled),
};
memo.set(record.connId, record);
upsertConnection(record);
return record;
}
/**
* Возвращает { ownerId, ownerChatId, ... } по connId или null.
* api — bot.api (для getBusinessConnection); можно не передавать, тогда только кэш/БД.
*/
export async function resolveOwner(connId, api) {
if (connId == null) return null;
const key = String(connId);
const cached = memo.get(key);
if (cached) return cached;
const row = getConnection(key);
if (row) {
const record = {
connId: key,
ownerId: row.owner_id ?? '',
ownerChatId: row.owner_chat_id ?? '',
ownerName: row.owner_name ?? '',
ownerUsername: row.owner_username ?? '',
isEnabled: Boolean(row.is_enabled),
};
memo.set(key, record);
return record;
}
if (api?.getBusinessConnection) {
try {
const conn = await api.getBusinessConnection(key);
if (conn?.id) return rememberConnection(conn);
} catch (err) {
logger.warn(`connections: getBusinessConnection(${key}) не удался:`, err.message);
}
}
return null;
}