This commit is contained in:
2026-07-13 00:06:51 -03:00
parent a4798f8db6
commit 09c1f57a34
17 changed files with 3799 additions and 0 deletions
+672
View File
@@ -0,0 +1,672 @@
package main
// bot_store.go — PostgreSQL persistence for the Telegram sales bot.
//
// Tables (all created idempotently by EnsureBotSchema):
// bot_users — Telegram customers/resellers
// bot_plans — sellable SSH/Xray plans
// bot_credit_packages — reseller credit top-up packages
// bot_transactions — PIX payments (Mercado Pago)
// bot_credits_ledger — auditable credit movements
// bot_settings — editable bot texts (key/value)
// bot_config — single-row bot config with encrypted secrets
import (
"context"
"database/sql"
"time"
"github.com/lib/pq"
)
// ---------- Models ----------
type BotUser struct {
TelegramID int64
Username string
FirstName string
Role string // customer | reseller | blocked
LinkedAdminUsername string
CreditBalance int
TrialUsed bool
CreatedAt time.Time
LastSeenAt time.Time
}
type BotPlan struct {
ID int
Name string
Kind string // ssh | xray
Days int
MaxConnections int
LimitMbpsUp int
LimitMbpsDown int
XrayInboundTag string
XrayProtocol string
PriceCents int
CreditCost int
ServerID string
IsActive bool
SortOrder int
}
type BotCreditPackage struct {
ID int
Name string
Credits int
PriceCents int
IsActive bool
SortOrder int
}
type BotTransaction struct {
ID int
TelegramID int64
Type string // plan_purchase | plan_renewal | credit_topup
PlanID *int
PackageID *int
Credits int
AmountCents int
MPPaymentID string
MPQRCode string
MPQRBase64 string
Status string // pending | approved | expired | refunded | error
TargetUsername string // account/uuid created or renewed
RenewTarget string // for renewals: existing account/uuid to extend
CreatedAt time.Time
PaidAt *time.Time
ExpiresAt *time.Time
}
type BotLedgerEntry struct {
ID int
TelegramID int64
Delta int
Reason string
RefTxnID *int
BalanceAfter int
CreatedAt time.Time
}
// ---------- Schema ----------
func (s *Store) EnsureBotSchema(ctx context.Context) error {
stmts := []string{
`CREATE TABLE IF NOT EXISTS bot_users (
telegram_id BIGINT PRIMARY KEY,
username TEXT NOT NULL DEFAULT '',
first_name TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL DEFAULT 'customer',
linked_admin_username TEXT NOT NULL DEFAULT '',
credit_balance INT NOT NULL DEFAULT 0,
trial_used BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`,
`CREATE TABLE IF NOT EXISTS bot_plans (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
kind TEXT NOT NULL DEFAULT 'ssh',
days INT NOT NULL DEFAULT 30,
max_connections INT NOT NULL DEFAULT 1,
limit_mbps_up INT NOT NULL DEFAULT 0,
limit_mbps_down INT NOT NULL DEFAULT 0,
xray_inbound_tag TEXT NOT NULL DEFAULT '',
xray_protocol TEXT NOT NULL DEFAULT '',
price_cents INT NOT NULL DEFAULT 0,
credit_cost INT NOT NULL DEFAULT 1,
server_id TEXT NOT NULL DEFAULT '',
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT NOT NULL DEFAULT 0
)`,
`CREATE TABLE IF NOT EXISTS bot_credit_packages (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
credits INT NOT NULL DEFAULT 0,
price_cents INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT NOT NULL DEFAULT 0
)`,
`CREATE TABLE IF NOT EXISTS bot_transactions (
id SERIAL PRIMARY KEY,
telegram_id BIGINT NOT NULL,
type TEXT NOT NULL,
plan_id INT,
package_id INT,
credits INT NOT NULL DEFAULT 0,
amount_cents INT NOT NULL DEFAULT 0,
mp_payment_id TEXT NOT NULL DEFAULT '',
mp_qr_code TEXT NOT NULL DEFAULT '',
mp_qr_base64 TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'pending',
target_username TEXT NOT NULL DEFAULT '',
renew_target TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
paid_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ
)`,
`CREATE UNIQUE INDEX IF NOT EXISTS bot_transactions_mp_payment_id_uidx
ON bot_transactions (mp_payment_id) WHERE mp_payment_id <> ''`,
`CREATE TABLE IF NOT EXISTS bot_credits_ledger (
id SERIAL PRIMARY KEY,
telegram_id BIGINT NOT NULL,
delta INT NOT NULL,
reason TEXT NOT NULL DEFAULT '',
ref_transaction_id INT,
balance_after INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`,
`CREATE TABLE IF NOT EXISTS bot_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL DEFAULT ''
)`,
`CREATE TABLE IF NOT EXISTS bot_config (
id INT PRIMARY KEY DEFAULT 1 CHECK (id = 1),
enabled BOOLEAN NOT NULL DEFAULT false,
telegram_mode TEXT NOT NULL DEFAULT 'polling',
telegram_webhook_url TEXT NOT NULL DEFAULT '',
mp_confirm_mode TEXT NOT NULL DEFAULT 'polling',
mp_poll_interval TEXT NOT NULL DEFAULT '20s',
pix_expiration_minutes INT NOT NULL DEFAULT 30,
trial_enabled BOOLEAN NOT NULL DEFAULT true,
trial_hours INT NOT NULL DEFAULT 1,
trial_max_connections INT NOT NULL DEFAULT 1,
trial_kind TEXT NOT NULL DEFAULT 'ssh',
trial_inbound_tag TEXT NOT NULL DEFAULT '',
admin_telegram_ids BIGINT[] NOT NULL DEFAULT '{}',
currency TEXT NOT NULL DEFAULT 'BRL',
public_host TEXT NOT NULL DEFAULT '',
xray_public_host TEXT NOT NULL DEFAULT '',
telegram_token_enc BYTEA,
telegram_webhook_secret_enc BYTEA,
mp_access_token_enc BYTEA,
mp_webhook_secret_enc BYTEA,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`,
`INSERT INTO bot_config (id) VALUES (1) ON CONFLICT (id) DO NOTHING`,
}
for _, stmt := range stmts {
if _, err := s.db.ExecContext(ctx, stmt); err != nil {
return err
}
}
return nil
}
// ---------- bot_users ----------
// UpsertBotUser inserts a user or refreshes username/first_name/last_seen.
// Role, credits, linkage and trial_used are preserved on update.
func (s *Store) UpsertBotUser(ctx context.Context, u *BotUser) error {
_, err := s.db.ExecContext(ctx, `
INSERT INTO bot_users (telegram_id, username, first_name, last_seen_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (telegram_id) DO UPDATE
SET username = EXCLUDED.username,
first_name = EXCLUDED.first_name,
last_seen_at = NOW()`,
u.TelegramID, u.Username, u.FirstName)
return err
}
func scanBotUser(row interface{ Scan(...interface{}) error }) (*BotUser, error) {
var u BotUser
err := row.Scan(&u.TelegramID, &u.Username, &u.FirstName, &u.Role,
&u.LinkedAdminUsername, &u.CreditBalance, &u.TrialUsed, &u.CreatedAt, &u.LastSeenAt)
if err != nil {
return nil, err
}
return &u, nil
}
const botUserCols = `telegram_id, username, first_name, role, linked_admin_username, credit_balance, trial_used, created_at, last_seen_at`
func (s *Store) GetBotUser(ctx context.Context, telegramID int64) (*BotUser, error) {
row := s.db.QueryRowContext(ctx, `SELECT `+botUserCols+` FROM bot_users WHERE telegram_id=$1`, telegramID)
return scanBotUser(row)
}
func (s *Store) ListBotUsers(ctx context.Context) ([]*BotUser, error) {
rows, err := s.db.QueryContext(ctx, `SELECT `+botUserCols+` FROM bot_users ORDER BY last_seen_at DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*BotUser
for rows.Next() {
u, err := scanBotUser(rows)
if err != nil {
return nil, err
}
out = append(out, u)
}
return out, rows.Err()
}
// SetBotUserRole updates role and reseller linkage.
func (s *Store) SetBotUserRole(ctx context.Context, telegramID int64, role, linkedAdmin string) error {
_, err := s.db.ExecContext(ctx,
`UPDATE bot_users SET role=$2, linked_admin_username=$3 WHERE telegram_id=$1`,
telegramID, role, linkedAdmin)
return err
}
func (s *Store) SetBotUserTrialUsed(ctx context.Context, telegramID int64) error {
_, err := s.db.ExecContext(ctx, `UPDATE bot_users SET trial_used=true WHERE telegram_id=$1`, telegramID)
return err
}
// AdjustCredits changes a reseller's balance atomically and writes a ledger row.
// Returns the resulting balance. Fails (rolls back) if the balance would go negative.
func (s *Store) AdjustCredits(ctx context.Context, telegramID int64, delta int, reason string, refTxn *int) (int, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return 0, err
}
defer tx.Rollback()
var balance int
if err := tx.QueryRowContext(ctx,
`UPDATE bot_users SET credit_balance = credit_balance + $2
WHERE telegram_id=$1 RETURNING credit_balance`,
telegramID, delta).Scan(&balance); err != nil {
return 0, err
}
if balance < 0 {
return 0, errInsufficientCredits
}
if _, err := tx.ExecContext(ctx,
`INSERT INTO bot_credits_ledger (telegram_id, delta, reason, ref_transaction_id, balance_after)
VALUES ($1,$2,$3,$4,$5)`,
telegramID, delta, reason, refTxn, balance); err != nil {
return 0, err
}
if err := tx.Commit(); err != nil {
return 0, err
}
return balance, nil
}
func (s *Store) ListLedger(ctx context.Context, telegramID int64, limit int) ([]*BotLedgerEntry, error) {
if limit <= 0 {
limit = 100
}
rows, err := s.db.QueryContext(ctx,
`SELECT id, telegram_id, delta, reason, ref_transaction_id, balance_after, created_at
FROM bot_credits_ledger WHERE telegram_id=$1 ORDER BY id DESC LIMIT $2`, telegramID, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*BotLedgerEntry
for rows.Next() {
var e BotLedgerEntry
if err := rows.Scan(&e.ID, &e.TelegramID, &e.Delta, &e.Reason, &e.RefTxnID, &e.BalanceAfter, &e.CreatedAt); err != nil {
return nil, err
}
out = append(out, &e)
}
return out, rows.Err()
}
// ---------- bot_plans ----------
const botPlanCols = `id, name, kind, days, max_connections, limit_mbps_up, limit_mbps_down, xray_inbound_tag, xray_protocol, price_cents, credit_cost, server_id, is_active, sort_order`
func scanBotPlan(row interface{ Scan(...interface{}) error }) (*BotPlan, error) {
var p BotPlan
err := row.Scan(&p.ID, &p.Name, &p.Kind, &p.Days, &p.MaxConnections, &p.LimitMbpsUp, &p.LimitMbpsDown,
&p.XrayInboundTag, &p.XrayProtocol, &p.PriceCents, &p.CreditCost, &p.ServerID, &p.IsActive, &p.SortOrder)
if err != nil {
return nil, err
}
return &p, nil
}
func (s *Store) ListPlans(ctx context.Context, onlyActive bool) ([]*BotPlan, error) {
q := `SELECT ` + botPlanCols + ` FROM bot_plans`
if onlyActive {
q += ` WHERE is_active=true`
}
q += ` ORDER BY sort_order, id`
rows, err := s.db.QueryContext(ctx, q)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*BotPlan
for rows.Next() {
p, err := scanBotPlan(rows)
if err != nil {
return nil, err
}
out = append(out, p)
}
return out, rows.Err()
}
func (s *Store) GetPlan(ctx context.Context, id int) (*BotPlan, error) {
row := s.db.QueryRowContext(ctx, `SELECT `+botPlanCols+` FROM bot_plans WHERE id=$1`, id)
return scanBotPlan(row)
}
func (s *Store) UpsertPlan(ctx context.Context, p *BotPlan) error {
if p.ID == 0 {
return s.db.QueryRowContext(ctx, `
INSERT INTO bot_plans (name, kind, days, max_connections, limit_mbps_up, limit_mbps_down,
xray_inbound_tag, xray_protocol, price_cents, credit_cost, server_id, is_active, sort_order)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING id`,
p.Name, p.Kind, p.Days, p.MaxConnections, p.LimitMbpsUp, p.LimitMbpsDown,
p.XrayInboundTag, p.XrayProtocol, p.PriceCents, p.CreditCost, p.ServerID, p.IsActive, p.SortOrder,
).Scan(&p.ID)
}
_, err := s.db.ExecContext(ctx, `
UPDATE bot_plans SET name=$2, kind=$3, days=$4, max_connections=$5, limit_mbps_up=$6, limit_mbps_down=$7,
xray_inbound_tag=$8, xray_protocol=$9, price_cents=$10, credit_cost=$11, server_id=$12, is_active=$13, sort_order=$14
WHERE id=$1`,
p.ID, p.Name, p.Kind, p.Days, p.MaxConnections, p.LimitMbpsUp, p.LimitMbpsDown,
p.XrayInboundTag, p.XrayProtocol, p.PriceCents, p.CreditCost, p.ServerID, p.IsActive, p.SortOrder)
return err
}
func (s *Store) DeletePlan(ctx context.Context, id int) error {
_, err := s.db.ExecContext(ctx, `DELETE FROM bot_plans WHERE id=$1`, id)
return err
}
// ---------- bot_credit_packages ----------
const botPkgCols = `id, name, credits, price_cents, is_active, sort_order`
func scanBotPkg(row interface{ Scan(...interface{}) error }) (*BotCreditPackage, error) {
var p BotCreditPackage
if err := row.Scan(&p.ID, &p.Name, &p.Credits, &p.PriceCents, &p.IsActive, &p.SortOrder); err != nil {
return nil, err
}
return &p, nil
}
func (s *Store) ListCreditPackages(ctx context.Context, onlyActive bool) ([]*BotCreditPackage, error) {
q := `SELECT ` + botPkgCols + ` FROM bot_credit_packages`
if onlyActive {
q += ` WHERE is_active=true`
}
q += ` ORDER BY sort_order, id`
rows, err := s.db.QueryContext(ctx, q)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*BotCreditPackage
for rows.Next() {
p, err := scanBotPkg(rows)
if err != nil {
return nil, err
}
out = append(out, p)
}
return out, rows.Err()
}
func (s *Store) GetCreditPackage(ctx context.Context, id int) (*BotCreditPackage, error) {
row := s.db.QueryRowContext(ctx, `SELECT `+botPkgCols+` FROM bot_credit_packages WHERE id=$1`, id)
return scanBotPkg(row)
}
func (s *Store) UpsertCreditPackage(ctx context.Context, p *BotCreditPackage) error {
if p.ID == 0 {
return s.db.QueryRowContext(ctx,
`INSERT INTO bot_credit_packages (name, credits, price_cents, is_active, sort_order)
VALUES ($1,$2,$3,$4,$5) RETURNING id`,
p.Name, p.Credits, p.PriceCents, p.IsActive, p.SortOrder).Scan(&p.ID)
}
_, err := s.db.ExecContext(ctx,
`UPDATE bot_credit_packages SET name=$2, credits=$3, price_cents=$4, is_active=$5, sort_order=$6 WHERE id=$1`,
p.ID, p.Name, p.Credits, p.PriceCents, p.IsActive, p.SortOrder)
return err
}
func (s *Store) DeleteCreditPackage(ctx context.Context, id int) error {
_, err := s.db.ExecContext(ctx, `DELETE FROM bot_credit_packages WHERE id=$1`, id)
return err
}
// ---------- bot_transactions ----------
const botTxnCols = `id, telegram_id, type, plan_id, package_id, credits, amount_cents, mp_payment_id, mp_qr_code, mp_qr_base64, status, target_username, renew_target, created_at, paid_at, expires_at`
func scanBotTxn(row interface{ Scan(...interface{}) error }) (*BotTransaction, error) {
var t BotTransaction
err := row.Scan(&t.ID, &t.TelegramID, &t.Type, &t.PlanID, &t.PackageID, &t.Credits, &t.AmountCents,
&t.MPPaymentID, &t.MPQRCode, &t.MPQRBase64, &t.Status, &t.TargetUsername, &t.RenewTarget,
&t.CreatedAt, &t.PaidAt, &t.ExpiresAt)
if err != nil {
return nil, err
}
return &t, nil
}
func (s *Store) CreateTransaction(ctx context.Context, t *BotTransaction) error {
return s.db.QueryRowContext(ctx, `
INSERT INTO bot_transactions (telegram_id, type, plan_id, package_id, credits, amount_cents,
mp_payment_id, mp_qr_code, mp_qr_base64, status, target_username, renew_target, expires_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING id, created_at`,
t.TelegramID, t.Type, t.PlanID, t.PackageID, t.Credits, t.AmountCents,
t.MPPaymentID, t.MPQRCode, t.MPQRBase64, t.Status, t.TargetUsername, t.RenewTarget, t.ExpiresAt,
).Scan(&t.ID, &t.CreatedAt)
}
func (s *Store) GetTransaction(ctx context.Context, id int) (*BotTransaction, error) {
row := s.db.QueryRowContext(ctx, `SELECT `+botTxnCols+` FROM bot_transactions WHERE id=$1`, id)
return scanBotTxn(row)
}
func (s *Store) GetTransactionByMPID(ctx context.Context, mpID string) (*BotTransaction, error) {
row := s.db.QueryRowContext(ctx, `SELECT `+botTxnCols+` FROM bot_transactions WHERE mp_payment_id=$1`, mpID)
return scanBotTxn(row)
}
func (s *Store) ListPendingTransactions(ctx context.Context) ([]*BotTransaction, error) {
rows, err := s.db.QueryContext(ctx,
`SELECT `+botTxnCols+` FROM bot_transactions WHERE status='pending' AND mp_payment_id <> '' ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*BotTransaction
for rows.Next() {
t, err := scanBotTxn(rows)
if err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
func (s *Store) ListUserTransactions(ctx context.Context, telegramID int64, limit int) ([]*BotTransaction, error) {
if limit <= 0 {
limit = 50
}
rows, err := s.db.QueryContext(ctx,
`SELECT `+botTxnCols+` FROM bot_transactions WHERE telegram_id=$1 ORDER BY id DESC LIMIT $2`, telegramID, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*BotTransaction
for rows.Next() {
t, err := scanBotTxn(rows)
if err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
func (s *Store) ListTransactions(ctx context.Context, status string, limit int) ([]*BotTransaction, error) {
if limit <= 0 {
limit = 200
}
var rows *sql.Rows
var err error
if status != "" {
rows, err = s.db.QueryContext(ctx, `SELECT `+botTxnCols+` FROM bot_transactions WHERE status=$1 ORDER BY id DESC LIMIT $2`, status, limit)
} else {
rows, err = s.db.QueryContext(ctx, `SELECT `+botTxnCols+` FROM bot_transactions ORDER BY id DESC LIMIT $1`, limit)
}
if err != nil {
return nil, err
}
defer rows.Close()
var out []*BotTransaction
for rows.Next() {
t, err := scanBotTxn(rows)
if err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
// MarkTransactionApproved atomically flips a pending txn to approved. It returns
// true only for the caller that actually performed the transition, giving
// idempotent delivery even if webhook and poller race.
func (s *Store) MarkTransactionApproved(ctx context.Context, id int) (bool, error) {
res, err := s.db.ExecContext(ctx,
`UPDATE bot_transactions SET status='approved', paid_at=NOW() WHERE id=$1 AND status='pending'`, id)
if err != nil {
return false, err
}
n, _ := res.RowsAffected()
return n == 1, nil
}
func (s *Store) SetTransactionStatus(ctx context.Context, id int, status string) error {
_, err := s.db.ExecContext(ctx, `UPDATE bot_transactions SET status=$2 WHERE id=$1`, id, status)
return err
}
func (s *Store) SetTransactionTarget(ctx context.Context, id int, target string) error {
_, err := s.db.ExecContext(ctx, `UPDATE bot_transactions SET target_username=$2 WHERE id=$1`, id, target)
return err
}
// ---------- bot_settings ----------
func (s *Store) GetSetting(ctx context.Context, key, def string) string {
var v string
err := s.db.QueryRowContext(ctx, `SELECT value FROM bot_settings WHERE key=$1`, key).Scan(&v)
if err != nil {
return def
}
return v
}
func (s *Store) SetSetting(ctx context.Context, key, value string) error {
_, err := s.db.ExecContext(ctx,
`INSERT INTO bot_settings (key, value) VALUES ($1,$2)
ON CONFLICT (key) DO UPDATE SET value=EXCLUDED.value`, key, value)
return err
}
func (s *Store) AllSettings(ctx context.Context) (map[string]string, error) {
rows, err := s.db.QueryContext(ctx, `SELECT key, value FROM bot_settings`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]string{}
for rows.Next() {
var k, v string
if err := rows.Scan(&k, &v); err != nil {
return nil, err
}
out[k] = v
}
return out, rows.Err()
}
// ---------- bot_config (row) ----------
// botConfigRow mirrors the DB row; secrets stay encrypted here.
type botConfigRow struct {
Enabled bool
TelegramMode string
TelegramWebhookURL string
MPConfirmMode string
MPPollInterval string
PixExpirationMinutes int
TrialEnabled bool
TrialHours int
TrialMaxConnections int
TrialKind string
TrialInboundTag string
AdminTelegramIDs []int64
Currency string
PublicHost string
XrayPublicHost string
TelegramTokenEnc []byte
TelegramWebhookSecretEnc []byte
MPAccessTokenEnc []byte
MPWebhookSecretEnc []byte
}
func (s *Store) getBotConfigRow(ctx context.Context) (*botConfigRow, error) {
var r botConfigRow
var ids pq.Int64Array
err := s.db.QueryRowContext(ctx, `
SELECT enabled, telegram_mode, telegram_webhook_url, mp_confirm_mode, mp_poll_interval,
pix_expiration_minutes, trial_enabled, trial_hours, trial_max_connections, trial_kind,
trial_inbound_tag, admin_telegram_ids, currency, public_host, xray_public_host,
telegram_token_enc, telegram_webhook_secret_enc, mp_access_token_enc, mp_webhook_secret_enc
FROM bot_config WHERE id=1`).Scan(
&r.Enabled, &r.TelegramMode, &r.TelegramWebhookURL, &r.MPConfirmMode, &r.MPPollInterval,
&r.PixExpirationMinutes, &r.TrialEnabled, &r.TrialHours, &r.TrialMaxConnections, &r.TrialKind,
&r.TrialInboundTag, &ids, &r.Currency, &r.PublicHost, &r.XrayPublicHost,
&r.TelegramTokenEnc, &r.TelegramWebhookSecretEnc, &r.MPAccessTokenEnc, &r.MPWebhookSecretEnc)
if err != nil {
return nil, err
}
r.AdminTelegramIDs = []int64(ids)
return &r, nil
}
// saveBotConfigRow writes the non-secret fields plus any encrypted blobs that
// are non-nil (nil blob = keep existing secret).
func (s *Store) saveBotConfigRow(ctx context.Context, r *botConfigRow, tokEnc, tgSecEnc, mpEnc, mpSecEnc []byte) error {
set := `enabled=$1, telegram_mode=$2, telegram_webhook_url=$3, mp_confirm_mode=$4, mp_poll_interval=$5,
pix_expiration_minutes=$6, trial_enabled=$7, trial_hours=$8, trial_max_connections=$9, trial_kind=$10,
trial_inbound_tag=$11, admin_telegram_ids=$12, currency=$13, public_host=$14, xray_public_host=$15,
updated_at=NOW()`
args := []interface{}{
r.Enabled, r.TelegramMode, r.TelegramWebhookURL, r.MPConfirmMode, r.MPPollInterval,
r.PixExpirationMinutes, r.TrialEnabled, r.TrialHours, r.TrialMaxConnections, r.TrialKind,
r.TrialInboundTag, pq.Array(r.AdminTelegramIDs), r.Currency, r.PublicHost, r.XrayPublicHost,
}
n := len(args)
if tokEnc != nil {
n++
set += `, telegram_token_enc=$` + botItoa(n)
args = append(args, tokEnc)
}
if tgSecEnc != nil {
n++
set += `, telegram_webhook_secret_enc=$` + botItoa(n)
args = append(args, tgSecEnc)
}
if mpEnc != nil {
n++
set += `, mp_access_token_enc=$` + botItoa(n)
args = append(args, mpEnc)
}
if mpSecEnc != nil {
n++
set += `, mp_webhook_secret_enc=$` + botItoa(n)
args = append(args, mpSecEnc)
}
_, err := s.db.ExecContext(ctx, `UPDATE bot_config SET `+set+` WHERE id=1`, args...)
return err
}