From aa5361c88b19b6750cedf200330142751d29a09f Mon Sep 17 00:00:00 2001 From: renovate Date: Fri, 9 Oct 2026 06:02:20 +0200 Subject: [PATCH 1/7] chore(deps): update dependency ultracite to v7.12.4 (dev) (#13) Co-authored-by: renovate --- frontend/bun.lock | 4 ++-- frontend/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index 3a1df13..c34b090 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -25,7 +25,7 @@ "svelte-check": "^4.7.6", "tailwindcss": "^4.2.2", "typescript": "^5.9.3", - "ultracite": "7.12.2", + "ultracite": "7.12.4", "vite": "^8.0.7", }, }, @@ -479,7 +479,7 @@ "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], - "ultracite": ["ultracite@7.12.2", "", { "dependencies": { "@clack/prompts": "^1.8.1", "cli-truncate": "^6.1.1", "commander": "^15.0.0", "deepmerge": "^4.3.1", "empathic": "^2.1.0", "execa": "^10.0.1", "fast-glob": "^3.3.3", "find-workspaces": "^0.3.1", "jsonc-parser": "^3.3.1", "log-update": "^8.0.0", "magicast": "^0.5.5", "nypm": "^0.6.10", "resolve.exports": "^2.0.3", "semver": "^7.8.5", "string-width": "^8.3.0", "yaml": "^2.9.1", "zod": "^4.6.5" }, "peerDependencies": { "@biomejs/biome": "^2.5.0", "eslint": "^10.0.0", "oxfmt": ">=0.59.0", "oxlint": "^1.82.0", "prettier": "^3.0.0", "stylelint": "^17.0.0" }, "optionalPeers": ["@biomejs/biome", "eslint", "oxfmt", "oxlint", "prettier", "stylelint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-0FQE7EuVFnX1FjtlIBklkfBTiZ5bJxknXSxcsjKos5Y73FlkasubWH7uVLS8jgOAI204B7xaIq+buE3vUybuwQ=="], + "ultracite": ["ultracite@7.12.4", "", { "dependencies": { "@clack/prompts": "^1.8.1", "cli-truncate": "^6.1.1", "commander": "^15.0.0", "deepmerge": "^4.3.1", "empathic": "^2.1.0", "execa": "^10.0.1", "fast-glob": "^3.3.3", "find-workspaces": "^0.3.1", "jsonc-parser": "^3.3.1", "log-update": "^8.0.0", "magicast": "^0.5.5", "npm-run-path": "^6.0.0", "nypm": "^0.6.10", "resolve.exports": "^2.0.3", "semver": "^7.8.5", "string-width": "^8.3.0", "which-command": "^0.1.0", "yaml": "^2.9.1", "zod": "^4.6.5" }, "peerDependencies": { "@biomejs/biome": "^2.5.0", "eslint": "^10.9.0", "oxfmt": ">=0.59.0", "oxlint": "^1.86.0", "prettier": "^3.0.0", "stylelint": "^17.0.0" }, "optionalPeers": ["@biomejs/biome", "eslint", "oxfmt", "oxlint", "prettier", "stylelint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-6jVEwaZBaytFm6XRoI3WGS2ZydrkrNvHTD0qTf7983ti2FlEWL1YcxnPaMaYuS6PkXqMhIK2XGr0cbsL4x5pQA=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], diff --git a/frontend/package.json b/frontend/package.json index fcfed10..0ceed33 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,7 +34,7 @@ "svelte-check": "^4.7.6", "tailwindcss": "^4.2.2", "typescript": "^5.9.3", - "ultracite": "7.12.2", + "ultracite": "7.12.4", "vite": "^8.0.7" } } -- 2.54.0 From e730b88d6db851843063b2152f353caa4412c526 Mon Sep 17 00:00:00 2001 From: renovate Date: Sat, 10 Oct 2026 00:31:18 +0000 Subject: [PATCH 2/7] chore(backend): update template to v0.6.0, conflicts left --- .copier/backend.yml | 2 +- backend/.env.example | 1 + backend/CLAUDE.md | 7 +++-- backend/Makefile | 20 +++--------- backend/migrations/env.py | 15 +++++++++ backend/scripts/check_imports.py | 44 +++++++++++++++++++++++++++ backend/scripts/session/create.py | 4 +++ backend/src/dependencies/container.py | 4 +++ backend/src/userbot/__init__.py | 3 ++ backend/src/utils/env.py | 20 ++++++++++++ backend/src/utils/imports.py | 6 ++++ backend/src/utils/logging.py | 4 +-- 12 files changed, 109 insertions(+), 21 deletions(-) create mode 100644 backend/scripts/check_imports.py create mode 100644 backend/src/utils/imports.py diff --git a/.copier/backend.yml b/.copier/backend.yml index d72ae20..f99b06c 100644 --- a/.copier/backend.yml +++ b/.copier/backend.yml @@ -1,4 +1,4 @@ -_commit: v0.4.0 +_commit: v0.6.0 _src_path: https://git.kotikot.com/templates/backend-python.git author_email: h@kotikot.com author_name: h diff --git a/backend/.env.example b/backend/.env.example index eafd9f2..098c87f 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -4,6 +4,7 @@ TZ=UTC LOG__LEVEL=INFO LOG__LEVEL_EXTERNAL=WARNING LOG__SHOW_TIME=false +LOG__SHOW_LOCALS=false LOG__CONSOLE_WIDTH=150 DB__HOST=postgres diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index 2cabb83..9f4b8ca 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -1,7 +1,8 @@ ## Checking commands After writing code, always run from `backend/`: ```shell -ruff format -ruff check --fix -ty check +make fmt +make check ``` + +Mind startup imports: `make check` runs `scripts/check_imports.py`. diff --git a/backend/Makefile b/backend/Makefile index cbcd6e6..99d7ec2 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -1,12 +1,15 @@ COMPOSE := cd .. && docker compose +<<<<<<< before updating IMAGE := beavergram/backend IMAGE_INPUTS := pyproject.toml uv.lock Dockerfile CODE := src scripts ../frontend SERVICES := api userbot watcher changed = ! git diff --quiet $(1) -- $(2) 2>/dev/null stamp = { git update-ref $(1) HEAD 2>/dev/null || true; } +======= +>>>>>>> after updating -.PHONY: fmt check build pre-deploy post-deploy migrate revision session +.PHONY: fmt check build migrate revision session fmt: uv run ruff format @@ -16,24 +19,11 @@ check: uv run ruff format --check uv run ruff check uv run ty check + uv run python scripts/check_imports.py build: $(COMPOSE) build api -pre-deploy: - @if $(call changed,refs/deploy/backend,$(IMAGE_INPUTS)) || ! docker image inspect $(IMAGE) >/dev/null 2>&1; then \ - $(MAKE) build && $(call stamp,refs/deploy/backend) && $(call stamp,refs/deploy/backend-code); \ - fi - @$(COMPOSE) config --services | grep -qx postgres && $(COMPOSE) up -d postgres || true - $(MAKE) migrate - -post-deploy: - @if $(call changed,refs/deploy/backend-code,$(CODE)); then \ - running=$$($(COMPOSE) ps --services --status running | grep -x $(addprefix -e ,$(SERVICES))); \ - [ -z "$$running" ] || $(COMPOSE) restart $$running; \ - $(call stamp,refs/deploy/backend-code); \ - fi - migrate: $(COMPOSE) run --rm migrator $(or $(filter-out $@,$(MAKECMDGOALS)),upgrade head) diff --git a/backend/migrations/env.py b/backend/migrations/env.py index 5dea3f2..bfe3016 100644 --- a/backend/migrations/env.py +++ b/backend/migrations/env.py @@ -16,14 +16,20 @@ if config.config_file_name is not None: target_metadata = SQLModel.metadata +<<<<<<< before updating def _async_url() -> str: return env.db.connection_url.replace("postgresql://", "postgresql+asyncpg://", 1) +======= +def include_name(name: str | None, type_: str, _parents: object) -> bool: + return not (type_ == "table" and name is not None and name.startswith("taskiq_")) +>>>>>>> after updating def run_migrations_offline() -> None: context.configure( url=_async_url(), target_metadata=target_metadata, + include_name=include_name, literal_binds=True, dialect_opts={"paramstyle": "named"}, ) @@ -31,8 +37,17 @@ def run_migrations_offline() -> None: context.run_migrations() +<<<<<<< before updating def _do_run_migrations(connection) -> None: # noqa: ANN001 context.configure(connection=connection, target_metadata=target_metadata) +======= +def do_run_migrations(connection: Connection) -> None: + context.configure( + connection=connection, + target_metadata=target_metadata, + include_name=include_name, + ) +>>>>>>> after updating with context.begin_transaction(): context.run_migrations() diff --git a/backend/scripts/check_imports.py b/backend/scripts/check_imports.py new file mode 100644 index 0000000..9878fb3 --- /dev/null +++ b/backend/scripts/check_imports.py @@ -0,0 +1,44 @@ +import os +import subprocess +import sys + +HEAVY = ["aiogram", "pyrogram", "fastapi", "google.genai"] +SERVICES = { + "api": (["api.app"], {"fastapi"}), + "userbot": (["userbot.runner", "userbot.handlers"], {"pyrogram"}), +} +PROBE = """ +import importlib, sys +for module in sys.argv[1].split(","): + importlib.import_module(module) +print(" ".join(name for name in sys.argv[2:] if name in sys.modules)) +""" +ENV = dict(os.environ) + + +def loaded(modules: list[str]) -> tuple[set[str], str]: + result = subprocess.run( # noqa: S603 + [sys.executable, "-c", PROBE, ",".join(modules), *HEAVY], + capture_output=True, + text=True, + cwd="src", + env=ENV, + check=False, + ) + error = (result.stderr.strip().splitlines() or ["exit code"])[-1] + return set(result.stdout.split()) & set(HEAVY), error if result.returncode else "" + + +def main() -> int: + failed = False + for service, (modules, allowed) in SERVICES.items(): + found, error = loaded(modules) + extra = sorted(found - allowed) + status = error or ("imports " + ", ".join(extra) if extra else "") + print(f"{service:10s} {'FAIL: ' + status if status else 'ok'}") + failed |= bool(status) + return int(failed) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/backend/scripts/session/create.py b/backend/scripts/session/create.py index d2194e4..0847237 100644 --- a/backend/scripts/session/create.py +++ b/backend/scripts/session/create.py @@ -5,6 +5,7 @@ from userbot.modules.client import PyroClient from utils.env import env from utils.logging import logger, setup_logging +<<<<<<< before updating setup_logging() @@ -18,6 +19,9 @@ def _finalize_session(sessions_dir: Path, name: str) -> Path: target = sessions_dir / f"{name}.session" (sessions_dir / "generated.session").rename(target) return target +======= +from userbot.modules.client import PyroClient +>>>>>>> after updating async def main() -> None: diff --git a/backend/src/dependencies/container.py b/backend/src/dependencies/container.py index f2cf878..7bd29b6 100644 --- a/backend/src/dependencies/container.py +++ b/backend/src/dependencies/container.py @@ -3,4 +3,8 @@ from dishka import make_async_container from dependencies.providers.postgres import DbProvider from dependencies.providers.storage import StorageProvider +<<<<<<< before updating container = make_async_container(DbProvider(), StorageProvider()) +======= +container = make_async_container(DbProvider(), RepositoryProvider()) +>>>>>>> after updating diff --git a/backend/src/userbot/__init__.py b/backend/src/userbot/__init__.py index 64586e2..f322d9e 100644 --- a/backend/src/userbot/__init__.py +++ b/backend/src/userbot/__init__.py @@ -1,3 +1,6 @@ +<<<<<<< before updating from userbot.modules.client import DEVICE_MODEL, DEVICE_MODEL_LIMIT, PyroClient __all__ = ["DEVICE_MODEL", "DEVICE_MODEL_LIMIT", "PyroClient"] +======= +>>>>>>> after updating diff --git a/backend/src/utils/env.py b/backend/src/utils/env.py index bc7cb3f..15097c9 100644 --- a/backend/src/utils/env.py +++ b/backend/src/utils/env.py @@ -4,7 +4,27 @@ from pydantic import Field, SecretStr from pydantic_settings import BaseSettings, SettingsConfigDict +<<<<<<< before updating class DatabaseSettings(BaseSettings): +======= +def is_prod() -> bool: + return os.getenv("RUN_ENVIRONMENT") == "prod" + + +class Section(BaseSettings): + model_config = SettingsConfigDict(extra="ignore") + + +class LogSettings(Section): + level: str = "INFO" + level_external: str = "WARNING" + show_time: bool = False + show_locals: bool = False + console_width: int = 150 + + +class DatabaseSettings(Section): +>>>>>>> after updating host: str = "postgres" port: int = 5432 user: str = "beavergram" diff --git a/backend/src/utils/imports.py b/backend/src/utils/imports.py new file mode 100644 index 0000000..b267223 --- /dev/null +++ b/backend/src/utils/imports.py @@ -0,0 +1,6 @@ +import asyncio +from importlib import import_module + + +async def preload(module: str) -> None: + await asyncio.to_thread(import_module, module) diff --git a/backend/src/utils/logging.py b/backend/src/utils/logging.py index 29ef942..906fecd 100644 --- a/backend/src/utils/logging.py +++ b/backend/src/utils/logging.py @@ -20,13 +20,13 @@ def setup_logging() -> None: markup=True, rich_tracebacks=True, enable_link_path=False, - tracebacks_show_locals=True, + tracebacks_show_locals=env.log.show_locals, omit_repeated_times=False, show_time=env.log.show_time, ) ], ) - install(console=console, show_locals=True) + install(console=console, show_locals=env.log.show_locals) logger = logging.getLogger("beavergram") -- 2.54.0 From fd429dd851dd95e211fecef2f3b3769a216d2333 Mon Sep 17 00:00:00 2001 From: renovate Date: Sat, 10 Oct 2026 00:31:21 +0000 Subject: [PATCH 3/7] chore(caddy): update template to v0.1.4 --- .copier/caddy.yml | 2 +- caddy/.gitignore | 2 +- caddy/README.md | 21 +-------------------- 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/.copier/caddy.yml b/.copier/caddy.yml index 680cbcd..4f40df6 100644 --- a/.copier/caddy.yml +++ b/.copier/caddy.yml @@ -1,4 +1,4 @@ -_commit: v0.1.2 +_commit: v0.1.4 _src_path: https://git.kotikot.com/templates/caddy.git project_slug: beavergram routes: diff --git a/caddy/.gitignore b/caddy/.gitignore index acd0b35..a9e5752 100644 --- a/caddy/.gitignore +++ b/caddy/.gitignore @@ -1,2 +1,2 @@ .env -Caddyfile +/Caddyfile diff --git a/caddy/README.md b/caddy/README.md index aa765fc..48dd517 100644 --- a/caddy/README.md +++ b/caddy/README.md @@ -1,22 +1,3 @@ # caddy -`site.caddy` routes `{$BEAVERGRAM_DOMAIN}` to the project's services over the external `caddy` docker network. It works standalone and inside a shared Caddy that fronts several projects. - -Standalone: - -```sh -docker network create caddy -cp Caddyfile.example Caddyfile -cp .env.example .env -docker compose up -d -``` - -Shared Caddy: mount this directory as `/etc/caddy/projects.d/beavergram:ro`, keep `import /etc/caddy/projects.d/*/site.caddy` in its Caddyfile and put `BEAVERGRAM_DOMAIN` in its environment. - -Rules that keep `site.caddy` portable: - -- the domain comes only from the environment variable -- no `tls` block, certificates are configured by the host Caddy (`acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}` in the global block) -- snippets are prefixed with the project slug because Caddy snippets are global -- mount the directory, not the file: a single-file bind mount goes stale when git replaces the inode -- keep `admin localhost:2019`, `admin off` makes `caddy reload` impossible +`site.caddy` routes `{$BEAVERGRAM_DOMAIN}` to the project over the external `caddy` network. In a shared Caddy, mount this directory as `/etc/caddy/projects.d/beavergram:ro` and set `BEAVERGRAM_DOMAIN`. Keep it portable: domain only from the env, no `tls`, snippets prefixed with `beavergram_`. -- 2.54.0 From 124a4329d618ac30b68e174655d116fa956e7a4d Mon Sep 17 00:00:00 2001 From: renovate Date: Sat, 10 Oct 2026 00:31:28 +0000 Subject: [PATCH 4/7] chore(frontend): update template to v0.6.0 --- .copier/frontend.yml | 3 ++- frontend/.dockerignore | 4 +++- frontend/Makefile | 12 +----------- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/.copier/frontend.yml b/.copier/frontend.yml index 741e72d..9c778c3 100644 --- a/.copier/frontend.yml +++ b/.copier/frontend.yml @@ -1,7 +1,8 @@ -_commit: v0.4.2 +_commit: v0.6.0 _src_path: https://git.kotikot.com/templates/frontend-svelte.git install_deps: false layout: part +linter: biome project_name: beavergram project_slug: beavergram serve: api diff --git a/frontend/.dockerignore b/frontend/.dockerignore index 1048241..ed46f37 100644 --- a/frontend/.dockerignore +++ b/frontend/.dockerignore @@ -1,5 +1,7 @@ node_modules .svelte-kit build -.env +# .env goes into the build: $env/static/public (and static/private) are +# inlined at build time from it. It stays in the builder stage, the runtime +# image gets build/ only. Local overrides (.env.local and the like) stay out. .env.* diff --git a/frontend/Makefile b/frontend/Makefile index 0cef562..484b133 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -1,8 +1,6 @@ COMPOSE := cd .. && docker compose -changed = ! git diff --quiet $(1) -- $(2) 2>/dev/null -stamp = { git update-ref $(1) HEAD 2>/dev/null || true; } -.PHONY: fmt check build pre-deploy post-deploy dev +.PHONY: fmt check build dev fmt: bun run fix @@ -14,13 +12,5 @@ check: build: $(COMPOSE) run --rm frontend-dev sh -c "bun install && bun run build" -pre-deploy: - @if $(call changed,refs/deploy/frontend,.) || ! test -d build; then \ - $(MAKE) build && $(call stamp,refs/deploy/frontend); \ - fi - -post-deploy: - @: - dev: bun run dev -- 2.54.0 From 698e4d60a323665ec1adc64c00e8436362dccc90 Mon Sep 17 00:00:00 2001 From: renovate Date: Sat, 10 Oct 2026 00:31:31 +0000 Subject: [PATCH 5/7] chore(infra): update template to v0.6.0, conflicts left --- .copier/infra.yml | 2 +- .gitignore | 2 +- Makefile | 8 ++------ README.md | 10 ++++++++++ 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.copier/infra.yml b/.copier/infra.yml index ac0d644..16c8b8d 100644 --- a/.copier/infra.yml +++ b/.copier/infra.yml @@ -1,4 +1,4 @@ -_commit: v0.5.0 +_commit: v0.6.0 _src_path: https://git.kotikot.com/templates/infra.git init_git: false layout: parts diff --git a/.gitignore b/.gitignore index ddf137d..b6ba220 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ docs storage # Local playground -t/ +/t/ diff --git a/Makefile b/Makefile index 6dd3380..07ff450 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PARTS := $(patsubst %/Makefile,%,$(wildcard */Makefile)) -.PHONY: env recreate rebuild restart logs down deploy fmt check build pre-deploy post-deploy +.PHONY: env recreate rebuild restart logs down fmt check build env: @fresh=; test -f .env || { awk 'FNR == 1 && NR > 1 { print "" } 1' .env.example $(addsuffix /.env.example,$(PARTS)) > .env; fresh=1; }; \ @@ -30,9 +30,5 @@ logs: down: docker compose down -deploy: pre-deploy - docker compose up -d - $(MAKE) post-deploy - -fmt check build pre-deploy post-deploy: +fmt check build: @for part in $(PARTS); do $(MAKE) -C $$part $@ || exit 1; done diff --git a/README.md b/README.md index 83cacb1..5db79f1 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ A read-only Telegram client that keeps everything. A userbot on your own account logs every incoming message into Postgres, including the ones later deleted or edited, along with media, reactions, presence and stories. A web client reads that archive back, and an MCP server hands it to agents. +<<<<<<< before updating Useful on its own: a Telegram where nothing disappears. In the [beaver](https://git.kotikot.com/beaver) setup it is one of the agent's hands - the dispatcher reads chat history by the `chat_id` stored in a person's card. ## What it keeps @@ -43,3 +44,12 @@ make fmt make check make -C frontend dev ``` +======= +```sh +make env # .env and docker-compose.override.yml from the examples +make recreate # the services in COMPOSE_PROFILES +make check +``` + +Parts: `backend/`, `frontend/`, each with its own Makefile. +>>>>>>> after updating -- 2.54.0 From 9e672623ac529ee22b05397f0e78efba8500be9e Mon Sep 17 00:00:00 2001 From: h Date: Sat, 10 Oct 2026 02:58:29 +0000 Subject: [PATCH 6/7] style(backend): one-line import, ruff 0.17 keeps the pragma out of the line width --- backend/src/userbot/modules/capture/identity.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/src/userbot/modules/capture/identity.py b/backend/src/userbot/modules/capture/identity.py index bccd633..fe1e4c1 100644 --- a/backend/src/userbot/modules/capture/identity.py +++ b/backend/src/userbot/modules/capture/identity.py @@ -41,9 +41,7 @@ class ChatMetaCache: chat_id: int, meta: tuple[str | None, str | None], ) -> bool: - from userbot.modules.groups.repository import ( # noqa: PLC0415 - get_latest_chat_meta, - ) + from userbot.modules.groups.repository import get_latest_chat_meta # noqa: PLC0415 if chat_id not in self._cache: self._cache[chat_id] = await get_latest_chat_meta(pool, account_id, chat_id) -- 2.54.0 From 4945c7cbd3a27e3c62329999e51d6773aa6a4e03 Mon Sep 17 00:00:00 2001 From: h Date: Sat, 10 Oct 2026 02:58:29 +0000 Subject: [PATCH 7/7] chore(backend,infra): resolve template update conflicts, import check knows the watcher and the api login --- README.md | 13 ++---------- backend/Makefile | 9 -------- backend/migrations/env.py | 10 ++------- backend/pyproject.toml | 1 + backend/scripts/check_imports.py | 5 +++-- backend/scripts/session/create.py | 4 ---- backend/src/api/login.py | 2 +- backend/src/api/routers/accounts.py | 2 +- backend/src/dependencies/container.py | 4 ---- backend/src/userbot/__init__.py | 6 ------ backend/src/userbot/handlers/deletes.py | 2 +- backend/src/userbot/handlers/edits.py | 2 +- backend/src/userbot/handlers/messages.py | 2 +- backend/src/userbot/handlers/presence.py | 2 +- backend/src/userbot/handlers/raw/contacts.py | 2 +- .../userbot/handlers/raw/dialog_filters.py | 2 +- .../src/userbot/handlers/raw/dispatcher.py | 2 +- backend/src/userbot/handlers/raw/profiles.py | 2 +- backend/src/userbot/handlers/raw/reactions.py | 2 +- .../src/userbot/handlers/raw/read_contents.py | 2 +- .../src/userbot/handlers/raw/read_receipts.py | 2 +- .../src/userbot/handlers/raw/transcribed.py | 2 +- backend/src/userbot/handlers/stories.py | 2 +- backend/src/userbot/runner.py | 2 +- backend/src/utils/env.py | 21 +------------------ backend/src/watcher/drain.py | 2 +- backend/src/watcher/registry.py | 2 +- 27 files changed, 27 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 5db79f1..0a81fa9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ A read-only Telegram client that keeps everything. A userbot on your own account logs every incoming message into Postgres, including the ones later deleted or edited, along with media, reactions, presence and stories. A web client reads that archive back, and an MCP server hands it to agents. -<<<<<<< before updating Useful on its own: a Telegram where nothing disappears. In the [beaver](https://git.kotikot.com/beaver) setup it is one of the agent's hands - the dispatcher reads chat history by the `chat_id` stored in a person's card. ## What it keeps @@ -32,7 +31,8 @@ Read-only: `list_watched_bots`, `get_bot_stats`, `get_bot_updates`, `search_mess ```sh make env # .env and docker-compose.override.yml from the examples make -C backend session # log the account in once; the session file stays in backend/sessions -make deploy # build what changed, migrate, start the containers +make -C backend migrate # the schema +make recreate # build and start the services in COMPOSE_PROFILES ``` `COMPOSE_PROFILES` in `.env` selects what runs on the machine, `docker-compose.override.yml` in each part holds the published ports and the `caddy` network aliases. The web client and the MCP endpoint are served by `api`; the bearer for both is `AUTH__TOKEN`. @@ -44,12 +44,3 @@ make fmt make check make -C frontend dev ``` -======= -```sh -make env # .env and docker-compose.override.yml from the examples -make recreate # the services in COMPOSE_PROFILES -make check -``` - -Parts: `backend/`, `frontend/`, each with its own Makefile. ->>>>>>> after updating diff --git a/backend/Makefile b/backend/Makefile index 99d7ec2..f5aef1f 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -1,13 +1,4 @@ COMPOSE := cd .. && docker compose -<<<<<<< before updating -IMAGE := beavergram/backend -IMAGE_INPUTS := pyproject.toml uv.lock Dockerfile -CODE := src scripts ../frontend -SERVICES := api userbot watcher -changed = ! git diff --quiet $(1) -- $(2) 2>/dev/null -stamp = { git update-ref $(1) HEAD 2>/dev/null || true; } -======= ->>>>>>> after updating .PHONY: fmt check build migrate revision session diff --git a/backend/migrations/env.py b/backend/migrations/env.py index bfe3016..fdbaa2d 100644 --- a/backend/migrations/env.py +++ b/backend/migrations/env.py @@ -16,13 +16,12 @@ if config.config_file_name is not None: target_metadata = SQLModel.metadata -<<<<<<< before updating def _async_url() -> str: return env.db.connection_url.replace("postgresql://", "postgresql+asyncpg://", 1) -======= + + def include_name(name: str | None, type_: str, _parents: object) -> bool: return not (type_ == "table" and name is not None and name.startswith("taskiq_")) ->>>>>>> after updating def run_migrations_offline() -> None: @@ -37,17 +36,12 @@ def run_migrations_offline() -> None: context.run_migrations() -<<<<<<< before updating def _do_run_migrations(connection) -> None: # noqa: ANN001 - context.configure(connection=connection, target_metadata=target_metadata) -======= -def do_run_migrations(connection: Connection) -> None: context.configure( connection=connection, target_metadata=target_metadata, include_name=include_name, ) ->>>>>>> after updating with context.begin_transaction(): context.run_migrations() diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 079ed12..1985075 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -39,6 +39,7 @@ unfixable = ["F401"] [tool.ruff.lint.per-file-ignores] "scripts/*" = ["INP"] +"scripts/check_imports.py" = ["T201"] "t/*" = ["ALL"] "*.lock" = ["ALL"] "migrations/*" = ["INP", "D", "E501"] diff --git a/backend/scripts/check_imports.py b/backend/scripts/check_imports.py index 9878fb3..c3831d9 100644 --- a/backend/scripts/check_imports.py +++ b/backend/scripts/check_imports.py @@ -4,8 +4,9 @@ import sys HEAVY = ["aiogram", "pyrogram", "fastapi", "google.genai"] SERVICES = { - "api": (["api.app"], {"fastapi"}), + "api": (["api.app"], {"fastapi", "pyrogram"}), "userbot": (["userbot.runner", "userbot.handlers"], {"pyrogram"}), + "watcher": (["watcher.runner"], {"pyrogram"}), } PROBE = """ import importlib, sys @@ -13,7 +14,7 @@ for module in sys.argv[1].split(","): importlib.import_module(module) print(" ".join(name for name in sys.argv[2:] if name in sys.modules)) """ -ENV = dict(os.environ) +ENV = {**os.environ, "AUTH__TOKEN": "check"} def loaded(modules: list[str]) -> tuple[set[str], str]: diff --git a/backend/scripts/session/create.py b/backend/scripts/session/create.py index 0847237..d2194e4 100644 --- a/backend/scripts/session/create.py +++ b/backend/scripts/session/create.py @@ -5,7 +5,6 @@ from userbot.modules.client import PyroClient from utils.env import env from utils.logging import logger, setup_logging -<<<<<<< before updating setup_logging() @@ -19,9 +18,6 @@ def _finalize_session(sessions_dir: Path, name: str) -> Path: target = sessions_dir / f"{name}.session" (sessions_dir / "generated.session").rename(target) return target -======= -from userbot.modules.client import PyroClient ->>>>>>> after updating async def main() -> None: diff --git a/backend/src/api/login.py b/backend/src/api/login.py index 52b5b7f..361f640 100644 --- a/backend/src/api/login.py +++ b/backend/src/api/login.py @@ -9,7 +9,7 @@ from pyrogram.errors import AuthTokenExpired, SessionPasswordNeeded from pyrogram.qrlogin import QRLogin from pyrogram.types import User -from userbot import PyroClient +from userbot.modules.client import PyroClient from utils.env import env LOGIN_TTL_SECONDS = 900 diff --git a/backend/src/api/routers/accounts.py b/backend/src/api/routers/accounts.py index 41fa8bf..5c69fd9 100644 --- a/backend/src/api/routers/accounts.py +++ b/backend/src/api/routers/accounts.py @@ -9,7 +9,7 @@ from pyrogram.errors import FloodWait, RPCError from pyrogram.types import User from api.login import LoginError, login_manager -from userbot import DEVICE_MODEL_LIMIT +from userbot.modules.client import DEVICE_MODEL_LIMIT from utils.read import accounts from utils.read.models import AccountView diff --git a/backend/src/dependencies/container.py b/backend/src/dependencies/container.py index 7bd29b6..f2cf878 100644 --- a/backend/src/dependencies/container.py +++ b/backend/src/dependencies/container.py @@ -3,8 +3,4 @@ from dishka import make_async_container from dependencies.providers.postgres import DbProvider from dependencies.providers.storage import StorageProvider -<<<<<<< before updating container = make_async_container(DbProvider(), StorageProvider()) -======= -container = make_async_container(DbProvider(), RepositoryProvider()) ->>>>>>> after updating diff --git a/backend/src/userbot/__init__.py b/backend/src/userbot/__init__.py index f322d9e..e69de29 100644 --- a/backend/src/userbot/__init__.py +++ b/backend/src/userbot/__init__.py @@ -1,6 +0,0 @@ -<<<<<<< before updating -from userbot.modules.client import DEVICE_MODEL, DEVICE_MODEL_LIMIT, PyroClient - -__all__ = ["DEVICE_MODEL", "DEVICE_MODEL_LIMIT", "PyroClient"] -======= ->>>>>>> after updating diff --git a/backend/src/userbot/handlers/deletes.py b/backend/src/userbot/handlers/deletes.py index 6927914..984835a 100644 --- a/backend/src/userbot/handlers/deletes.py +++ b/backend/src/userbot/handlers/deletes.py @@ -1,8 +1,8 @@ from pyrogram.types import Message -from userbot import PyroClient from userbot.modules.capture import repository from userbot.modules.capture.repository import CHANNEL_ID_THRESHOLD +from userbot.modules.client import PyroClient from utils.events import notify_bg_event diff --git a/backend/src/userbot/handlers/edits.py b/backend/src/userbot/handlers/edits.py index 4b7e192..1afe136 100644 --- a/backend/src/userbot/handlers/edits.py +++ b/backend/src/userbot/handlers/edits.py @@ -1,9 +1,9 @@ from pyrogram.types import Message -from userbot import PyroClient from userbot.modules.capture import repository from userbot.modules.capture.chat_meta import meta_from_chat from userbot.modules.capture.message import sender_id +from userbot.modules.client import PyroClient from userbot.modules.media import capture_media, media_unique_id, self_destruct_ttl from userbot.modules.raw_json import raw_json from utils.events import notify_bg_event diff --git a/backend/src/userbot/handlers/messages.py b/backend/src/userbot/handlers/messages.py index 03494a1..be9998e 100644 --- a/backend/src/userbot/handlers/messages.py +++ b/backend/src/userbot/handlers/messages.py @@ -1,8 +1,8 @@ from pyrogram.types import Message -from userbot import PyroClient from userbot.modules.capture import capture_message from userbot.modules.capture.chat_meta import meta_from_chat +from userbot.modules.client import PyroClient from userbot.modules.stt import is_transcribable from userbot.modules.stt.gate import safe_transcribe from utils.events import notify_bg_event diff --git a/backend/src/userbot/handlers/presence.py b/backend/src/userbot/handlers/presence.py index 7672e1f..1b29c48 100644 --- a/backend/src/userbot/handlers/presence.py +++ b/backend/src/userbot/handlers/presence.py @@ -2,7 +2,7 @@ from datetime import UTC, datetime from pyrogram.types import User -from userbot import PyroClient +from userbot.modules.client import PyroClient from userbot.modules.presence import repository from userbot.modules.raw_json import raw_json from utils.events import notify_bg_event diff --git a/backend/src/userbot/handlers/raw/contacts.py b/backend/src/userbot/handlers/raw/contacts.py index 6655705..8e86c61 100644 --- a/backend/src/userbot/handlers/raw/contacts.py +++ b/backend/src/userbot/handlers/raw/contacts.py @@ -1,6 +1,6 @@ from pyrogram import raw -from userbot import PyroClient +from userbot.modules.client import PyroClient HANDLES = (raw.types.UpdateContactsReset, raw.types.UpdateUser) diff --git a/backend/src/userbot/handlers/raw/dialog_filters.py b/backend/src/userbot/handlers/raw/dialog_filters.py index ff802b2..af47585 100644 --- a/backend/src/userbot/handlers/raw/dialog_filters.py +++ b/backend/src/userbot/handlers/raw/dialog_filters.py @@ -1,6 +1,6 @@ from pyrogram import raw -from userbot import PyroClient +from userbot.modules.client import PyroClient HANDLES = ( raw.types.UpdateDialogFilter, diff --git a/backend/src/userbot/handlers/raw/dispatcher.py b/backend/src/userbot/handlers/raw/dispatcher.py index 79c547d..c6e3e31 100644 --- a/backend/src/userbot/handlers/raw/dispatcher.py +++ b/backend/src/userbot/handlers/raw/dispatcher.py @@ -2,7 +2,6 @@ from collections.abc import Awaitable, Callable from pyrogram import raw -from userbot import PyroClient from userbot.handlers.raw import ( contacts, dialog_filters, @@ -12,6 +11,7 @@ from userbot.handlers.raw import ( read_receipts, transcribed, ) +from userbot.modules.client import PyroClient RawHandler = Callable[..., Awaitable[None]] diff --git a/backend/src/userbot/handlers/raw/profiles.py b/backend/src/userbot/handlers/raw/profiles.py index bc46f47..5e76d87 100644 --- a/backend/src/userbot/handlers/raw/profiles.py +++ b/backend/src/userbot/handlers/raw/profiles.py @@ -2,8 +2,8 @@ from dataclasses import replace from pyrogram import raw -from userbot import PyroClient from userbot.modules.avatars import capture_avatar +from userbot.modules.client import PyroClient from userbot.modules.profiles import active_username, snapshot_from_user from userbot.modules.profiles.parse import ProfileFields from userbot.modules.profiles.repository import get_peer, write_profile diff --git a/backend/src/userbot/handlers/raw/reactions.py b/backend/src/userbot/handlers/raw/reactions.py index d0a209c..3e944fe 100644 --- a/backend/src/userbot/handlers/raw/reactions.py +++ b/backend/src/userbot/handlers/raw/reactions.py @@ -1,8 +1,8 @@ from pyrogram import raw, utils -from userbot import PyroClient from userbot.modules.capture import repository from userbot.modules.capture.chat_meta import meta_from_peer +from userbot.modules.client import PyroClient from utils.events import notify_bg_event HANDLES = (raw.types.UpdateMessageReactions,) diff --git a/backend/src/userbot/handlers/raw/read_contents.py b/backend/src/userbot/handlers/raw/read_contents.py index 9f85cbd..56dd922 100644 --- a/backend/src/userbot/handlers/raw/read_contents.py +++ b/backend/src/userbot/handlers/raw/read_contents.py @@ -2,8 +2,8 @@ from datetime import UTC, datetime from pyrogram import raw, utils -from userbot import PyroClient from userbot.modules.capture.chat_meta import meta_from_chat_id +from userbot.modules.client import PyroClient from userbot.modules.raw_json import raw_json from userbot.modules.read_receipts import repository as receipts from userbot.modules.stt import repository diff --git a/backend/src/userbot/handlers/raw/read_receipts.py b/backend/src/userbot/handlers/raw/read_receipts.py index ec5ba6b..43e9ccf 100644 --- a/backend/src/userbot/handlers/raw/read_receipts.py +++ b/backend/src/userbot/handlers/raw/read_receipts.py @@ -2,7 +2,7 @@ from datetime import UTC, datetime from pyrogram import raw, utils -from userbot import PyroClient +from userbot.modules.client import PyroClient from userbot.modules.raw_json import raw_json from userbot.modules.read_receipts import repository from utils.events import notify_bg_event diff --git a/backend/src/userbot/handlers/raw/transcribed.py b/backend/src/userbot/handlers/raw/transcribed.py index 78460ae..335743b 100644 --- a/backend/src/userbot/handlers/raw/transcribed.py +++ b/backend/src/userbot/handlers/raw/transcribed.py @@ -1,7 +1,7 @@ from pyrogram import raw -from userbot import PyroClient from userbot.modules.capture.chat_meta import meta_from_peer +from userbot.modules.client import PyroClient from userbot.modules.stt import repository HANDLES = (raw.types.UpdateTranscribedAudio,) diff --git a/backend/src/userbot/handlers/stories.py b/backend/src/userbot/handlers/stories.py index c0d9ccf..93385de 100644 --- a/backend/src/userbot/handlers/stories.py +++ b/backend/src/userbot/handlers/stories.py @@ -1,6 +1,6 @@ from pyrogram.types import Story -from userbot import PyroClient +from userbot.modules.client import PyroClient from userbot.modules.stories.service import save_story diff --git a/backend/src/userbot/runner.py b/backend/src/userbot/runner.py index bf45952..36f8da7 100644 --- a/backend/src/userbot/runner.py +++ b/backend/src/userbot/runner.py @@ -10,8 +10,8 @@ import asyncpg import uvloop from dependencies.container import container -from userbot import PyroClient from userbot.modules.capture import CaptureContext, build_capture_context +from userbot.modules.client import PyroClient from userbot.modules.jobs import JobConsumer from utils.env import env from utils.jobs import enqueue diff --git a/backend/src/utils/env.py b/backend/src/utils/env.py index 15097c9..f945eb6 100644 --- a/backend/src/utils/env.py +++ b/backend/src/utils/env.py @@ -4,27 +4,7 @@ from pydantic import Field, SecretStr from pydantic_settings import BaseSettings, SettingsConfigDict -<<<<<<< before updating class DatabaseSettings(BaseSettings): -======= -def is_prod() -> bool: - return os.getenv("RUN_ENVIRONMENT") == "prod" - - -class Section(BaseSettings): - model_config = SettingsConfigDict(extra="ignore") - - -class LogSettings(Section): - level: str = "INFO" - level_external: str = "WARNING" - show_time: bool = False - show_locals: bool = False - console_width: int = 150 - - -class DatabaseSettings(Section): ->>>>>>> after updating host: str = "postgres" port: int = 5432 user: str = "beavergram" @@ -76,6 +56,7 @@ class LogSettings(BaseSettings): level: str = "INFO" level_external: str = "WARNING" show_time: bool = False + show_locals: bool = False console_width: int = 150 diff --git a/backend/src/watcher/drain.py b/backend/src/watcher/drain.py index 634a69a..17c12e0 100644 --- a/backend/src/watcher/drain.py +++ b/backend/src/watcher/drain.py @@ -13,10 +13,10 @@ from dataclasses import dataclass from pyrogram import raw, types from pyrogram.errors import RPCError -from userbot import PyroClient from userbot.modules.capture import capture_message from userbot.modules.capture.chat_meta import meta_from_chat from userbot.modules.capture.context import CaptureContext +from userbot.modules.client import PyroClient from utils.logging import logger EARLIEST_PTS = 1 diff --git a/backend/src/watcher/registry.py b/backend/src/watcher/registry.py index 039f21c..54094d6 100644 --- a/backend/src/watcher/registry.py +++ b/backend/src/watcher/registry.py @@ -11,9 +11,9 @@ from pyrogram.handlers import ( MessageHandler, ) -from userbot import PyroClient from userbot.modules.capture import capture_message from userbot.modules.capture.chat_meta import meta_from_chat +from userbot.modules.client import PyroClient from userbot.modules.jobs import JobConsumer from utils.events import notify_bg_event from utils.jobs import enqueue -- 2.54.0