Compare commits

..
2 Commits
28 changed files with 28 additions and 85 deletions

No files matched your search

+2 -11
View File
@@ -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
-9
View File
@@ -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
+2 -8
View File
@@ -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()
+1
View File
@@ -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"]
+3 -2
View File
@@ -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]:
-4
View File
@@ -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:
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
-4
View File
@@ -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
-6
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
@@ -1,6 +1,6 @@
from pyrogram import raw
from userbot import PyroClient
from userbot.modules.client import PyroClient
HANDLES = (
raw.types.UpdateDialogFilter,
@@ -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]]
+1 -1
View File
@@ -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
@@ -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,)
@@ -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
@@ -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
@@ -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,)
+1 -1
View File
@@ -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
@@ -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)
+1 -1
View File
@@ -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
+1 -20
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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