A game bot for group chats that let its users grow their virtual "dicks" every day for some random count of centimeters (including negative values) and compete with friends and other chat members.
(compared with some competitors)
- The Dick of the Day daily contest to grow a randomly chosen dick for a bit more.
- A way to play the game without the necessity to add the bot into a group (via inline queries with a callback button).
- Import from @pipisabot and @kraft28_bot (not tested! help of its users is required).
- PvP fights with statistics.
- an option to show mercy and return the award for the battle back;
- support for those who loses battles the most;
- more perks;
- achievements;
- referral promo codes;
- global monthly events;
- a shop.
- true system random from the environment's chaos by usage of the
get_random()syscall (BCryptGenRandomon Windows, or other alternatives on different OSes); - localizations for English, Russian, Italian, Persian, and Chinese (Simplified & Traditional), switchable per chat via
/language— and per user too when the optional user-service is enabled; - Prometheus-like metrics, including per-function request/error/latency metrics via autometrics;
- OpenTelemetry distributed tracing (OTLP/gRPC), exportable to a collector such as Jaeger;
/supportto reach the owner without exposing an email or a personal account, and SQL functions to answer a data deletion request by hand — see Support requests and data deletion in the wiki;- can be restricted for use in specific topics only;
- optional self-destruction of the bot's own messages (and of the commands behind them, where the
bot is an administrator) to keep a busy chat readable — configured per message group with the
MSG_SELFDESTRUCT_*variables of .env.example, and by each chat's administrators for their own chat with/cleanup, which offers them a choice of delays per message group.
- PostgreSQL;
- [optional] Docker (it makes the configuration a lot easier);
- [optional] Task to start everything with a single command (see Running locally below);
- [optional] the user-service microservice for cross-bot user languages (see user-service integration below);
- [for webhook mode] a frontal proxy server with TLS support (nginx-proxy, for example).
cargo build/cargo check type-check SQL queries at compile time via sqlx. Unless
you're relying on the offline query cache (see below), this means your local database
schema must already match migrations/ — sqlx::migrate! only applies migrations
automatically when the bot itself starts, not at build time. If a build fails with
confusing SQL-query type-mismatch errors after pulling or adding a migration, apply
pending migrations first (requires sqlx-cli, cargo install sqlx-cli):
cargo sqlx migrate runThe usual dev setup runs the infrastructure in Docker (PostgreSQL, and optionally the
user-service) while you run the bot itself as a local binary. docker-compose.override.yml
forwards the containers' ports to localhost, so the binary reaches PostgreSQL at
localhost:5432 and the user-service at localhost:${USER_SERVICE_GRPC_PORT}.
Copy the example config first: cp .env.example .env (then fill in TELOXIDE_TOKEN).
With Task installed, one command brings the infra up, migrates the database and starts the bot:
task run| Task | What it does |
|---|---|
task infra |
Start the full infra — PostgreSQL + user-service — in Docker (ports forwarded to localhost); alias for task infra:full |
task infra:min |
Start only PostgreSQL (no user-service) |
task run |
task infra, apply migrations, then run the bot with cargo run |
task migrate |
Apply pending DB migrations (cargo sqlx migrate run) |
task up |
Build and start the whole stack (bot included) in Docker |
task down |
Stop and remove the Docker stack |
task infra:down |
Stop just the infra containers |
Without Task, the equivalent of task run is:
docker compose up -d --wait postgres user-service
cargo sqlx migrate run
cargo rundocker-compose.override.yml is loaded automatically by any docker compose command; it's
what forwards the container ports to localhost for the local binary. task up/down opt
out of it (via -f docker-compose.yml) to run the whole stack in Docker — but note that
running the bot inside Docker needs a Docker-oriented .env (POSTGRES_HOST=postgres,
and GRPC_ADDR_USER_SERVICE=user-service:${USER_SERVICE_GRPC_PORT} if the integration is on),
which is the opposite of the localhost values .env.example ships for the local-binary flow.
The bot logs and traces through tracing, with structured fields: a log
message is a constant and the values around it (chat_id, uid, error, …) are separate fields,
so the log database can filter and count them instead of matching text. RUST_LOG controls the
verbosity, of both the console and the export.
Console output is always on — it is the fallback when the observability stack is not configured or cannot be reached. Two more variables send the data to that stack, each optional and independent:
OTEL_EXPORTER_OTLP_ENDPOINT— the spans, over OTLP/gRPC;OTEL_EXPORTER_OTLP_LOGS_ENDPOINT— the log records, over OTLP/HTTP (the full URL, path included). The exported records carry the trace and span ids of the span they were written in, put there by the SDK, so a log line and a trace can be matched.
docker-compose.yml bundles an optional observability stack, gated behind the tracing Compose
profile: Jaeger all-in-one for the spans and
VictoriaLogs for the records. The
task infra/infra:full tasks start both automatically (they name them, which activates the
profile), and docker-compose.override.yml forwards their ports to localhost — so in the
local-binary dev flow the Jaeger UI is at http://localhost:16686, OTLP
at localhost:4317, and the VictoriaLogs UI at
http://localhost:9428/select/vmui/. For the
full-stack-in-Docker flow (task up, which skips the override), enable it with
COMPOSE_PROFILES=tracing; there they stay on the Compose network and the in-Docker bot reaches
them at jaeger:4317 and victoria-logs:9428. Set the two variables accordingly — see
.env.example, which ships both forms commented out.
The trace of an update is rooted at the handler that processes it, not at the HTTP request that brought it in: the webhook handler only parses the update and hands it to the dispatcher, so an HTTP span would be empty. Outbound user-service (gRPC) calls are auto-instrumented, and the trace context propagates into the user-service.
Independently of tracing, the /metrics endpoint (port 8080) exposes Prometheus metrics: HTTP
metrics from axum-prometheus, the bot's own domain counters, and per-function request/error/latency
metrics generated by autometrics on the handlers and repository methods.
Every call the bot makes to the Telegram Bot API is measured as well:
telegram_request_duration_seconds{method,outcome} records how long it took (the failed ones
included, so a timeout doesn't go missing), it gets a client span of its own in the trace, and a
request the API rejects without a proper error code is logged together with the payload that caused
it.
Everything the bot keeps briefly — cached settings, locks, unfinished dialogues — lives in one store instead of a map per feature. Almost nothing there is a source of truth, so a store that is down, slow or absent costs only the work it would have saved: the bot starts and runs either way.
Without REDIS_HOST the values stay in the process, which is all a single instance needs and not a
degraded mode. A server adds a dialogue that survives a restart, and correctness once there is more
than one instance, for a round trip on each miss. The container runs Valkey,
the BSD-licensed fork; the protocol is Redis, which the variables are named after, and
.env.example describes them.
user-service integration
user-service is a small gRPC microservice that stores a user's preferred interface language
and shares it across all of SadBot.Dev's bots. When enabled, the bot offers a personal
/language command in private chats and honours each user's saved language. When disabled,
languages come from Telegram as before and /language is hidden from private chats — the
chat-wide /language for group admins keeps working regardless, since it's stored in the
bot's own database.
docker-compose.yml bundles the user-service container, gated behind the user-service Compose
profile — the task infra/infra:full tasks start it automatically (they name it, which activates
its profile), while a bare docker compose up / task up starts it only when
COMPOSE_PROFILES=user-service is set. It shares the bot's PostgreSQL server but uses its own
userservicedb database, provisioned by postgres/init-user-service-db.sh (which only runs on a
fresh data volume — on an existing ./data, create the database once by hand; the script's
header shows the command).
Enable the integration by setting GRPC_ADDR_USER_SERVICE in .env, choosing the host by
where the bot runs:
- bot as a local binary (infra in Docker):
GRPC_ADDR_USER_SERVICE=localhost:${USER_SERVICE_GRPC_PORT}; - bot inside docker-compose:
GRPC_ADDR_USER_SERVICE=user-service:${USER_SERVICE_GRPC_PORT}.
See the user-service block in .env.example for the related variables (cache TTLs, gRPC
timeout, and the service's database credentials).
(to build the application without a running RDBMS)
cargo sqlx prepare -- --testsIt's most probably you want to change the value of the GROW_SHRINK_RATIO environment variable to make the players upset and disappointed more or less often.
The reward for playing every day is STREAK_BONUS_RATIO_PER_DAY (0.05 by default): each consecutive
day multiplies the rolled value by that much more, counting up to STREAK_BONUS_MAX_DAYS (20) days,
so three weeks of playing doubles it. A negative roll is multiplied just the same. Setting either
variable to 0 turns the perk off, as does DISABLE_STREAK.
Most of the command can be hidden from both lists: command hints and inline results. To do so, specify an environment variable like DISABLE_CMD_STATS (where STATS is a command key) with any value.
Don't forget to pass this variable to the container by adding it to the docker-compose.yml file!