Compare commits

...
24 changed files with 135 additions and 67 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
_commit: v0.4.0 _commit: v0.6.0
_src_path: https://git.kotikot.com/templates/backend-python.git _src_path: https://git.kotikot.com/templates/backend-python.git
author_email: h@kotikot.com author_email: h@kotikot.com
author_name: h author_name: h
+1 -1
View File
@@ -1,4 +1,4 @@
_commit: v0.1.2 _commit: v0.1.4
_src_path: https://git.kotikot.com/templates/caddy.git _src_path: https://git.kotikot.com/templates/caddy.git
project_slug: beavergram project_slug: beavergram
routes: routes:
+2 -1
View File
@@ -1,7 +1,8 @@
_commit: v0.4.2 _commit: v0.6.0
_src_path: https://git.kotikot.com/templates/frontend-svelte.git _src_path: https://git.kotikot.com/templates/frontend-svelte.git
install_deps: false install_deps: false
layout: part layout: part
linter: biome
project_name: beavergram project_name: beavergram
project_slug: beavergram project_slug: beavergram
serve: api serve: api
+1 -1
View File
@@ -1,4 +1,4 @@
_commit: v0.5.0 _commit: v0.6.0
_src_path: https://git.kotikot.com/templates/infra.git _src_path: https://git.kotikot.com/templates/infra.git
init_git: false init_git: false
layout: parts layout: parts
+1 -1
View File
@@ -8,4 +8,4 @@ docs
storage storage
# Local playground # Local playground
t/ /t/
+2 -6
View File
@@ -1,6 +1,6 @@
PARTS := $(patsubst %/Makefile,%,$(wildcard */Makefile)) 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: env:
@fresh=; test -f .env || { awk 'FNR == 1 && NR > 1 { print "" } 1' .env.example $(addsuffix /.env.example,$(PARTS)) > .env; fresh=1; }; \ @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: down:
docker compose down docker compose down
deploy: pre-deploy fmt check build:
docker compose up -d
$(MAKE) post-deploy
fmt check build pre-deploy post-deploy:
@for part in $(PARTS); do $(MAKE) -C $$part $@ || exit 1; done @for part in $(PARTS); do $(MAKE) -C $$part $@ || exit 1; done
+10
View File
@@ -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. 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. 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 ## What it keeps
@@ -43,3 +44,12 @@ make fmt
make check make check
make -C frontend dev 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
+1
View File
@@ -4,6 +4,7 @@ TZ=UTC
LOG__LEVEL=INFO LOG__LEVEL=INFO
LOG__LEVEL_EXTERNAL=WARNING LOG__LEVEL_EXTERNAL=WARNING
LOG__SHOW_TIME=false LOG__SHOW_TIME=false
LOG__SHOW_LOCALS=false
LOG__CONSOLE_WIDTH=150 LOG__CONSOLE_WIDTH=150
DB__HOST=postgres DB__HOST=postgres
+4 -3
View File
@@ -1,7 +1,8 @@
## Checking commands ## Checking commands
After writing code, always run from `backend/`: After writing code, always run from `backend/`:
```shell ```shell
ruff format make fmt
ruff check --fix make check
ty check
``` ```
Mind startup imports: `make check` runs `scripts/check_imports.py`.
+5 -15
View File
@@ -1,12 +1,15 @@
COMPOSE := cd .. && docker compose COMPOSE := cd .. && docker compose
<<<<<<< before updating
IMAGE := beavergram/backend IMAGE := beavergram/backend
IMAGE_INPUTS := pyproject.toml uv.lock Dockerfile IMAGE_INPUTS := pyproject.toml uv.lock Dockerfile
CODE := src scripts ../frontend CODE := src scripts ../frontend
SERVICES := api userbot watcher SERVICES := api userbot watcher
changed = ! git diff --quiet $(1) -- $(2) 2>/dev/null changed = ! git diff --quiet $(1) -- $(2) 2>/dev/null
stamp = { git update-ref $(1) HEAD 2>/dev/null || true; } 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: fmt:
uv run ruff format uv run ruff format
@@ -16,24 +19,11 @@ check:
uv run ruff format --check uv run ruff format --check
uv run ruff check uv run ruff check
uv run ty check uv run ty check
uv run python scripts/check_imports.py
build: build:
$(COMPOSE) build api $(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: migrate:
$(COMPOSE) run --rm migrator $(or $(filter-out $@,$(MAKECMDGOALS)),upgrade head) $(COMPOSE) run --rm migrator $(or $(filter-out $@,$(MAKECMDGOALS)),upgrade head)
+15
View File
@@ -16,14 +16,20 @@ if config.config_file_name is not None:
target_metadata = SQLModel.metadata target_metadata = SQLModel.metadata
<<<<<<< before updating
def _async_url() -> str: def _async_url() -> str:
return env.db.connection_url.replace("postgresql://", "postgresql+asyncpg://", 1) 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: def run_migrations_offline() -> None:
context.configure( context.configure(
url=_async_url(), url=_async_url(),
target_metadata=target_metadata, target_metadata=target_metadata,
include_name=include_name,
literal_binds=True, literal_binds=True,
dialect_opts={"paramstyle": "named"}, dialect_opts={"paramstyle": "named"},
) )
@@ -31,8 +37,17 @@ def run_migrations_offline() -> None:
context.run_migrations() context.run_migrations()
<<<<<<< before updating
def _do_run_migrations(connection) -> None: # noqa: ANN001 def _do_run_migrations(connection) -> None: # noqa: ANN001
context.configure(connection=connection, target_metadata=target_metadata) 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(): with context.begin_transaction():
context.run_migrations() context.run_migrations()
+44
View File
@@ -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())
+4
View File
@@ -5,6 +5,7 @@ from userbot.modules.client import PyroClient
from utils.env import env from utils.env import env
from utils.logging import logger, setup_logging from utils.logging import logger, setup_logging
<<<<<<< before updating
setup_logging() setup_logging()
@@ -18,6 +19,9 @@ def _finalize_session(sessions_dir: Path, name: str) -> Path:
target = sessions_dir / f"{name}.session" target = sessions_dir / f"{name}.session"
(sessions_dir / "generated.session").rename(target) (sessions_dir / "generated.session").rename(target)
return target return target
=======
from userbot.modules.client import PyroClient
>>>>>>> after updating
async def main() -> None: async def main() -> None:
+4
View File
@@ -3,4 +3,8 @@ from dishka import make_async_container
from dependencies.providers.postgres import DbProvider from dependencies.providers.postgres import DbProvider
from dependencies.providers.storage import StorageProvider from dependencies.providers.storage import StorageProvider
<<<<<<< before updating
container = make_async_container(DbProvider(), StorageProvider()) container = make_async_container(DbProvider(), StorageProvider())
=======
container = make_async_container(DbProvider(), RepositoryProvider())
>>>>>>> after updating
+3
View File
@@ -1,3 +1,6 @@
<<<<<<< before updating
from userbot.modules.client import DEVICE_MODEL, DEVICE_MODEL_LIMIT, PyroClient from userbot.modules.client import DEVICE_MODEL, DEVICE_MODEL_LIMIT, PyroClient
__all__ = ["DEVICE_MODEL", "DEVICE_MODEL_LIMIT", "PyroClient"] __all__ = ["DEVICE_MODEL", "DEVICE_MODEL_LIMIT", "PyroClient"]
=======
>>>>>>> after updating
+20
View File
@@ -4,7 +4,27 @@ from pydantic import Field, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
<<<<<<< before updating
class DatabaseSettings(BaseSettings): 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" host: str = "postgres"
port: int = 5432 port: int = 5432
user: str = "beavergram" user: str = "beavergram"
+6
View File
@@ -0,0 +1,6 @@
import asyncio
from importlib import import_module
async def preload(module: str) -> None:
await asyncio.to_thread(import_module, module)
+2 -2
View File
@@ -20,13 +20,13 @@ def setup_logging() -> None:
markup=True, markup=True,
rich_tracebacks=True, rich_tracebacks=True,
enable_link_path=False, enable_link_path=False,
tracebacks_show_locals=True, tracebacks_show_locals=env.log.show_locals,
omit_repeated_times=False, omit_repeated_times=False,
show_time=env.log.show_time, 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") logger = logging.getLogger("beavergram")
+1 -1
View File
@@ -1,2 +1,2 @@
.env .env
Caddyfile /Caddyfile
+1 -20
View File
@@ -1,22 +1,3 @@
# caddy # 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. `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_`.
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
+3 -1
View File
@@ -1,5 +1,7 @@
node_modules node_modules
.svelte-kit .svelte-kit
build 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.* .env.*
+1 -11
View File
@@ -1,8 +1,6 @@
COMPOSE := cd .. && docker compose 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: fmt:
bun run fix bun run fix
@@ -14,13 +12,5 @@ check:
build: build:
$(COMPOSE) run --rm frontend-dev sh -c "bun install && bun run 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: dev:
bun run dev bun run dev
+2 -2
View File
@@ -25,7 +25,7 @@
"svelte-check": "^4.7.6", "svelte-check": "^4.7.6",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.2",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"ultracite": "7.12.2", "ultracite": "7.12.4",
"vite": "^8.0.7", "vite": "^8.0.7",
}, },
}, },
@@ -479,7 +479,7 @@
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], "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=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
+1 -1
View File
@@ -34,7 +34,7 @@
"svelte-check": "^4.7.6", "svelte-check": "^4.7.6",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.2",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"ultracite": "7.12.2", "ultracite": "7.12.4",
"vite": "^8.0.7" "vite": "^8.0.7"
} }
} }