flake
This commit is contained in:
parent
8a76cca5fb
commit
7eb4977b99
6 changed files with 537 additions and 5 deletions
71
CLAUDE.md
Normal file
71
CLAUDE.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the app (requires CONFIG_PATH env var)
|
||||||
|
make run
|
||||||
|
# or explicitly:
|
||||||
|
CONFIG_PATH=./internal/config/local.yml go run ./cmd/app/main.go
|
||||||
|
|
||||||
|
# Build binary
|
||||||
|
go build -o crypto_alert_bot ./cmd/app/main.go
|
||||||
|
|
||||||
|
# Start PostgreSQL via Docker
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Create a new migration (NAME=migration_name)
|
||||||
|
make migrate_create NAME=add_alerts_user_id
|
||||||
|
# Edit the generated .up.sql and .down.sql in internal/repository/postgresql/migrations/
|
||||||
|
# Migrations run automatically on startup — no manual migrate up needed
|
||||||
|
```
|
||||||
|
|
||||||
|
There are no tests in this codebase.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Go Telegram bot that monitors cryptocurrency prices from Bybit and notifies users when price thresholds are hit.
|
||||||
|
|
||||||
|
### Layer structure (clean architecture)
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd/app/main.go → entrypoint: wires config, logger, storage, usecase, bot, alerter
|
||||||
|
internal/config/ → Config struct loaded from YAML via CONFIG_PATH env var
|
||||||
|
internal/entities/ → domain types: User, Pair, Price, Alert, Candle (no business logic)
|
||||||
|
internal/provider/ → Provider interface + Bybit REST implementation
|
||||||
|
internal/repository/ → Storage interface + PostgreSQL implementation (pgxpool)
|
||||||
|
internal/usecase/ → business logic; depends on repository.Storage interface
|
||||||
|
internal/service/alerter/ → background price-checking service; fires and disables alerts
|
||||||
|
internal/bot/telegram/ → Telegram bot: command handlers, inline keyboards, multi-step flows
|
||||||
|
internal/logger/ → wraps charmbracelet/log returning *slog.Logger
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key design decisions
|
||||||
|
|
||||||
|
- **Config**: YAML at `$CONFIG_PATH`. Local dev config at `internal/config/local.yml`. Sections: `logger`, `postgresql`, `telegram`, `providers.bybit`.
|
||||||
|
- **Migrations**: Embedded via `go:embed` in `internal/repository/postgresql/migrations/embed.go`, applied automatically on startup via `golang-migrate`.
|
||||||
|
- **Alerter service** (`internal/service/alerter/`): Runs a goroutine on a 1-minute ticker. On each tick, it fetches OHLC candles (not live price) for the gap since `lastCheckedAt`, using `selectCandleInterval` to pick the coarsest interval that fits in one Bybit request (≤ 1000 candles). Alerts trigger if any candle's High/Low crosses the target. `lastCheckedAt` is persisted to `alerter_state` so missed candles are rechecked after restarts.
|
||||||
|
- **In-memory alert cache** (`alertsCache`): Keyed by `AlertID` and `InstrumentID`. Only instruments with cached alerts have candles fetched. Cache is loaded from DB on startup via `LoadAlerts`, and kept in sync via `AddAlert`/`RemoveAlert` when the bot creates/removes alerts.
|
||||||
|
- **Circular dependency** between `Bot` and `Alerter`: resolved by constructing both independently and injecting `Alerter` into `Bot` via `bot.SetAlerter(al)` after both are created. `Bot` implements the `alerter.Notifier` interface (`NotifyAlert`), which the `Alerter` uses to send Telegram messages when an alert fires.
|
||||||
|
- **Alert condition auto-detection**: When a user sets a target price, the condition (`above`/`below`) is inferred automatically — if target ≥ current ask → `above`, else → `below`. Candle High is checked for `above`, candle Low for `below`.
|
||||||
|
- **Bot user state**: Per-user `userState` (protected by `sync.Mutex`) tracks multi-step flows (`stepAddAlertPrice`, `stepEditAlertPrice`). State is reset on completion, error, or `/cancel`.
|
||||||
|
- **Decimal arithmetic**: Prices use `github.com/shopspring/decimal` to avoid floating-point precision issues. Alert prices are stored as `text` in PostgreSQL.
|
||||||
|
- **Storage interface** (`internal/repository/repository.go`): Decouples usecase layer from PostgreSQL; implement this to swap databases.
|
||||||
|
|
||||||
|
### Database schema
|
||||||
|
|
||||||
|
- `users` — Telegram users (UUID PK, unique `telegram_id bigint`)
|
||||||
|
- `currency` — currency symbols (e.g. BTC, USDT); pre-seeded in migration
|
||||||
|
- `instrument` — trading pairs (base + quote currency FK); pre-seeded with BTC/USDT, ETH/USDT, SOL/USDT
|
||||||
|
- `alert` — price alerts per user/instrument with `active` flag and `alert_condition` enum; deactivated when triggered
|
||||||
|
- `alerter_state` — single-row table storing `last_alert_check timestamptz`; persists the timestamp across restarts so the alerter can fetch candles for any missed interval
|
||||||
|
|
||||||
|
### Bot commands / flows
|
||||||
|
|
||||||
|
- `/start` — register user
|
||||||
|
- `/instruments` (or "Instruments" button) — list available trading pairs
|
||||||
|
- `/add_alert` (or "Add Alert" button) — multi-step: select instrument → enter price → alert created
|
||||||
|
- `/my_alerts` (or "My Alerts" button) — list active alerts with inline Edit/Remove buttons
|
||||||
|
- `/cancel` — abort current multi-step flow
|
||||||
204
NIX.md
Normal file
204
NIX.md
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
# NixOS Deployment
|
||||||
|
|
||||||
|
## Flake outputs
|
||||||
|
|
||||||
|
| Output | Description |
|
||||||
|
|---|---|
|
||||||
|
| `packages.default` | The `crypto-alert-bot` binary |
|
||||||
|
| `devShells.default` | Dev shell with Go toolchain, gopls, golangci-lint, go-migrate |
|
||||||
|
| `nixosModules.default` | NixOS module that runs the bot as a systemd service |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NixOS module
|
||||||
|
|
||||||
|
The module handles all infrastructure automatically:
|
||||||
|
|
||||||
|
- Enables and configures `services.postgresql`
|
||||||
|
- Creates the `crypto-alert-bot` database and PostgreSQL role
|
||||||
|
- Connects via Unix socket using peer authentication (no password)
|
||||||
|
- Creates a dedicated system user `crypto-alert-bot`
|
||||||
|
- Runs the bot as a hardened systemd service
|
||||||
|
|
||||||
|
The only secret you need to supply is the Telegram bot token, via a file path.
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `enable` | bool | `false` | Enable the service |
|
||||||
|
| `package` | package | flake default | Override the binary package |
|
||||||
|
| `telegramTokenFile` | path | — | **Required.** Path to a file containing the bot token |
|
||||||
|
| `dbName` | string | `"crypto-alert-bot"` | PostgreSQL database name |
|
||||||
|
| `logLevel` | `debug\|info\|warn\|error` | `"info"` | Log verbosity |
|
||||||
|
| `logEncoding` | `console\|json` | `"json"` | Log format |
|
||||||
|
| `logServiceName` | string | `"alert-bot"` | Service name field in log output |
|
||||||
|
| `bybitBaseUrl` | string | `"https://api.bybit.com"` | Bybit REST API base URL |
|
||||||
|
|
||||||
|
### Minimal configuration
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# flake.nix of your NixOS config
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
crypto-alert-bot.url = "github:youruser/crypto_alert_bot";
|
||||||
|
agenix.url = "github:ryantm/agenix";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { nixpkgs, crypto-alert-bot, agenix, ... }: {
|
||||||
|
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
|
||||||
|
modules = [
|
||||||
|
crypto-alert-bot.nixosModules.default
|
||||||
|
agenix.nixosModules.default
|
||||||
|
./configuration.nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# configuration.nix
|
||||||
|
services.crypto-alert-bot = {
|
||||||
|
enable = true;
|
||||||
|
telegramTokenFile = config.age.secrets.telegram-token.path;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Encrypting the Telegram token with agenix
|
||||||
|
|
||||||
|
[agenix](https://github.com/ryantm/agenix) encrypts secrets with [age](https://github.com/FiloSottile/age)
|
||||||
|
and decrypts them at boot using SSH host keys already present on the machine.
|
||||||
|
Encrypted secret files are safe to commit to your NixOS config repository.
|
||||||
|
|
||||||
|
### 1. Install age and agenix
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# age — encryption tool
|
||||||
|
nix shell nixpkgs#age
|
||||||
|
|
||||||
|
# agenix CLI — manages encrypted secret files
|
||||||
|
nix shell github:ryantm/agenix
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Collect recipient public keys
|
||||||
|
|
||||||
|
agenix encrypts each secret for one or more recipients. Typical recipients:
|
||||||
|
|
||||||
|
- **SSH host key** of the target machine (used for decryption at boot)
|
||||||
|
- **Your personal SSH key** (used so you can re-encrypt or rotate secrets from your workstation)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get the host's SSH public key (run on the target machine or from your config)
|
||||||
|
cat /etc/ssh/ssh_host_ed25519_key.pub
|
||||||
|
|
||||||
|
# Get your personal SSH public key
|
||||||
|
cat ~/.ssh/id_ed25519.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create secrets.nix
|
||||||
|
|
||||||
|
In the root of your NixOS config repository, create `secrets/secrets.nix`.
|
||||||
|
This file declares which keys can decrypt each secret — it is **not** sensitive
|
||||||
|
and should be committed.
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# secrets/secrets.nix
|
||||||
|
let
|
||||||
|
# SSH public key of the target host
|
||||||
|
host = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... root@myhost";
|
||||||
|
|
||||||
|
# Your personal SSH public key (for re-encryption from your workstation)
|
||||||
|
me = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... user@workstation";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
"telegram-token.age".publicKeys = [ host me ];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Encrypt the token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd secrets/
|
||||||
|
|
||||||
|
# agenix will open $EDITOR so you can type the token, then encrypt on save
|
||||||
|
agenix -e telegram-token.age
|
||||||
|
```
|
||||||
|
|
||||||
|
Type (or paste) the raw token value — just the token string, no quotes, no newline:
|
||||||
|
|
||||||
|
```
|
||||||
|
1234567890:ABCdefGHIjklMNOpqrSTUvwxYZ
|
||||||
|
```
|
||||||
|
|
||||||
|
Save and close the editor. agenix writes `telegram-token.age` — this encrypted
|
||||||
|
file is safe to commit.
|
||||||
|
|
||||||
|
### 5. Wire the secret into your NixOS config
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# configuration.nix
|
||||||
|
{ config, ... }:
|
||||||
|
{
|
||||||
|
# Declare the secret; agenix decrypts it at boot to /run/agenix/telegram-token
|
||||||
|
age.secrets.telegram-token = {
|
||||||
|
file = ./secrets/telegram-token.age;
|
||||||
|
# The service user must be able to read the decrypted file
|
||||||
|
owner = "crypto-alert-bot";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
|
|
||||||
|
services.crypto-alert-bot = {
|
||||||
|
enable = true;
|
||||||
|
telegramTokenFile = config.age.secrets.telegram-token.path;
|
||||||
|
# Optionally tune logging:
|
||||||
|
# logLevel = "debug";
|
||||||
|
# logEncoding = "console";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nixos-rebuild switch --flake .#myhost
|
||||||
|
```
|
||||||
|
|
||||||
|
At boot, agenix decrypts `telegram-token.age` using the host's SSH private key
|
||||||
|
and places the plaintext at `/run/agenix/telegram-token` (mode 0400, owned by
|
||||||
|
`crypto-alert-bot`). The bot service reads the token from that path at start time.
|
||||||
|
|
||||||
|
### Rotating the token
|
||||||
|
|
||||||
|
Edit the encrypted file from your workstation with your personal SSH key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd secrets/
|
||||||
|
agenix -e telegram-token.age # opens $EDITOR with decrypted content
|
||||||
|
# change the value, save, close
|
||||||
|
git add telegram-token.age && git commit -m "rotate telegram token"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then redeploy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nixos-rebuild switch --flake .#myhost
|
||||||
|
systemctl restart crypto-alert-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development shell
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix develop
|
||||||
|
|
||||||
|
# Start the database locally (Docker or Podman)
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Run the bot
|
||||||
|
CONFIG_PATH=./internal/config/local.yml go run ./cmd/app/main.go
|
||||||
|
```
|
||||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1771848320,
|
||||||
|
"narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "2fc6539b481e1d2569f25f8799236694180c0993",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
187
flake.nix
Normal file
187
flake.nix
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
{
|
||||||
|
description = "Crypto alert Telegram bot that monitors Bybit prices";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages.default = pkgs.buildGoModule {
|
||||||
|
pname = "crypto-alert-bot";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
vendorHash = "sha256-toXJCeMHa61YCFIYtsTl4dime015AF5LlB62QmYVaA8=";
|
||||||
|
|
||||||
|
subPackages = [ "cmd/app" ];
|
||||||
|
|
||||||
|
meta = with pkgs.lib; {
|
||||||
|
description = "Telegram bot for Bybit cryptocurrency price alerts";
|
||||||
|
mainProgram = "app";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
go
|
||||||
|
gopls
|
||||||
|
gotools
|
||||||
|
golangci-lint
|
||||||
|
go-migrate # provides the `migrate` CLI
|
||||||
|
];
|
||||||
|
shellHook = ''
|
||||||
|
echo "crypto-alert-bot dev shell"
|
||||||
|
echo " Run: CONFIG_PATH=./internal/config/local.yml go run ./cmd/app/main.go"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) // {
|
||||||
|
# NixOS module — import this in your NixOS config to run the bot as a service.
|
||||||
|
#
|
||||||
|
# The module manages PostgreSQL automatically (creates the DB and user,
|
||||||
|
# connects via Unix socket with peer auth). The only settings you must
|
||||||
|
# provide are the Telegram token file and optionally log/provider tuning.
|
||||||
|
#
|
||||||
|
# Minimal example:
|
||||||
|
# services.crypto-alert-bot = {
|
||||||
|
# enable = true;
|
||||||
|
# telegramTokenFile = "/run/secrets/telegram-token";
|
||||||
|
# };
|
||||||
|
nixosModules.default = { config, lib, pkgs, ... }:
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
cfg = config.services.crypto-alert-bot;
|
||||||
|
svcUser = "crypto-alert-bot";
|
||||||
|
|
||||||
|
# The config YAML is fully static (no secrets); the Telegram token is
|
||||||
|
# read from a file at ExecStartPre time and written into a per-run copy.
|
||||||
|
staticConfig = pkgs.writeText "crypto-alert-bot-base.yml" ''
|
||||||
|
logger:
|
||||||
|
service_name: "${cfg.logServiceName}"
|
||||||
|
encoding: "${cfg.logEncoding}"
|
||||||
|
level: "${cfg.logLevel}"
|
||||||
|
postgresql:
|
||||||
|
address: "/run/postgresql"
|
||||||
|
user: "${svcUser}"
|
||||||
|
db_name: "${cfg.dbName}"
|
||||||
|
telegram:
|
||||||
|
token: "__TELEGRAM_TOKEN__"
|
||||||
|
providers:
|
||||||
|
bybit:
|
||||||
|
base_url: "${cfg.bybitBaseUrl}"
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Runs as root (+ prefix) before the main process. Copies the static
|
||||||
|
# config into the runtime directory and splices in the Telegram token.
|
||||||
|
configSetupScript = pkgs.writeShellScript "crypto-alert-bot-setup-config" ''
|
||||||
|
set -euo pipefail
|
||||||
|
token=$(< ${lib.escapeShellArg cfg.telegramTokenFile})
|
||||||
|
install -m 600 -o ${svcUser} ${staticConfig} /run/crypto-alert-bot/config.yml
|
||||||
|
sed -i "s|__TELEGRAM_TOKEN__|$token|" /run/crypto-alert-bot/config.yml
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.crypto-alert-bot = {
|
||||||
|
enable = mkEnableOption "crypto alert bot";
|
||||||
|
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = self.packages.${pkgs.system}.default;
|
||||||
|
defaultText = literalExpression "self.packages.\${system}.default";
|
||||||
|
description = "The crypto-alert-bot package to use.";
|
||||||
|
};
|
||||||
|
|
||||||
|
dbName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = svcUser;
|
||||||
|
description = "PostgreSQL database name (created automatically).";
|
||||||
|
};
|
||||||
|
|
||||||
|
telegramTokenFile = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
description = "Path to a file containing the Telegram bot token (e.g. managed by agenix or sops-nix).";
|
||||||
|
};
|
||||||
|
|
||||||
|
logServiceName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "alert-bot";
|
||||||
|
description = "Service name printed in log lines.";
|
||||||
|
};
|
||||||
|
|
||||||
|
logEncoding = mkOption {
|
||||||
|
type = types.enum [ "console" "json" ];
|
||||||
|
default = "json";
|
||||||
|
description = "Log output encoding.";
|
||||||
|
};
|
||||||
|
|
||||||
|
logLevel = mkOption {
|
||||||
|
type = types.enum [ "debug" "info" "warn" "error" ];
|
||||||
|
default = "info";
|
||||||
|
description = "Minimum log level.";
|
||||||
|
};
|
||||||
|
|
||||||
|
bybitBaseUrl = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "https://api.bybit.com";
|
||||||
|
description = "Bybit REST API base URL.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
# Create a stable system user whose name matches the PostgreSQL role
|
||||||
|
# so that peer (Unix socket) authentication works without a password.
|
||||||
|
users.users.${svcUser} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = svcUser;
|
||||||
|
description = "Crypto alert bot service user";
|
||||||
|
};
|
||||||
|
users.groups.${svcUser} = {};
|
||||||
|
|
||||||
|
# Provision the database and role automatically.
|
||||||
|
services.postgresql = {
|
||||||
|
enable = true;
|
||||||
|
ensureDatabases = [ cfg.dbName ];
|
||||||
|
ensureUsers = [{
|
||||||
|
name = svcUser;
|
||||||
|
ensureDBOwnership = true;
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.crypto-alert-bot = {
|
||||||
|
description = "Crypto Alert Telegram Bot";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "network.target" "postgresql.service" ];
|
||||||
|
requires = [ "postgresql.service" ];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
# '+' prefix → root, so we can write to the RuntimeDirectory
|
||||||
|
# before switching to the unprivileged service user.
|
||||||
|
ExecStartPre = "+${configSetupScript}";
|
||||||
|
ExecStart = "${cfg.package}/bin/app";
|
||||||
|
Environment = "CONFIG_PATH=/run/crypto-alert-bot/config.yml";
|
||||||
|
|
||||||
|
User = svcUser;
|
||||||
|
Group = svcUser;
|
||||||
|
|
||||||
|
RuntimeDirectory = "crypto-alert-bot";
|
||||||
|
RuntimeDirectoryMode = "0700";
|
||||||
|
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "5s";
|
||||||
|
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -33,7 +33,7 @@ type Bybit struct {
|
||||||
type Postgresql struct {
|
type Postgresql struct {
|
||||||
Address string `yaml:"address" env-required:"true"`
|
Address string `yaml:"address" env-required:"true"`
|
||||||
User string `yaml:"user" env-required:"true"`
|
User string `yaml:"user" env-required:"true"`
|
||||||
Password string `yaml:"password" env-required:"true"`
|
Password string `yaml:"password" env-default:""` // empty for peer auth over Unix socket
|
||||||
DBName string `yaml:"db_name" env-required:"true"`
|
DBName string `yaml:"db_name" env-required:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitea.computernetthings.ru/yash/crypto_alert_bot/internal/config"
|
"gitea.computernetthings.ru/yash/crypto_alert_bot/internal/config"
|
||||||
"gitea.computernetthings.ru/yash/crypto_alert_bot/internal/repository/postgresql/migrations"
|
"gitea.computernetthings.ru/yash/crypto_alert_bot/internal/repository/postgresql/migrations"
|
||||||
|
|
@ -22,8 +23,18 @@ type Postgresql struct {
|
||||||
db *pgxpool.Pool
|
db *pgxpool.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dsn builds a connection string that supports both TCP (host:port) and Unix
|
||||||
|
// socket (path starting with "/") addresses. Unix socket mode skips the
|
||||||
|
// password and uses OS peer authentication instead.
|
||||||
|
func dsn(cfg *config.Postgresql) string {
|
||||||
|
if strings.HasPrefix(cfg.Address, "/") {
|
||||||
|
return fmt.Sprintf("host=%s user=%s dbname=%s sslmode=disable", cfg.Address, cfg.User, cfg.DBName)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", cfg.User, cfg.Password, cfg.Address, cfg.DBName)
|
||||||
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, log *slog.Logger, cfg *config.Postgresql) (*Postgresql, error) {
|
func New(ctx context.Context, log *slog.Logger, cfg *config.Postgresql) (*Postgresql, error) {
|
||||||
pool, err := pgxpool.New(ctx, fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", cfg.User, cfg.Password, cfg.Address, cfg.DBName))
|
pool, err := pgxpool.New(ctx, dsn(cfg))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to postgres: %w", err)
|
return nil, fmt.Errorf("failed to connect to postgres: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -41,9 +52,7 @@ func New(ctx context.Context, log *slog.Logger, cfg *config.Postgresql) (*Postgr
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyMigrations(cfg *config.Postgresql, log *slog.Logger) error {
|
func applyMigrations(cfg *config.Postgresql, log *slog.Logger) error {
|
||||||
dsn := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", cfg.User, cfg.Password, cfg.Address, cfg.DBName)
|
sqlDB, err := sql.Open("pgx", dsn(cfg))
|
||||||
|
|
||||||
sqlDB, err := sql.Open("pgx", dsn)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open sql db for migrations: %w", err)
|
return fmt.Errorf("failed to open sql db for migrations: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue