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. 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
@@ -32,7 +31,8 @@ Read-only: `list_watched_bots`, `get_bot_stats`, `get_bot_updates`, `search_mess
```sh ```sh
make env # .env and docker-compose.override.yml from the examples 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 -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`. `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 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
-9
View File
@@ -1,13 +1,4 @@
COMPOSE := cd .. && docker compose 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 .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 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: def include_name(name: str | None, type_: str, _parents: object) -> bool:
return not (type_ == "table" and name is not None and name.startswith("taskiq_")) 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:
@@ -37,17 +36,12 @@ 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)
=======
def do_run_migrations(connection: Connection) -> None:
context.configure( context.configure(
connection=connection, connection=connection,
target_metadata=target_metadata, target_metadata=target_metadata,
include_name=include_name, include_name=include_name,
) )
>>>>>>> after updating
with context.begin_transaction(): with context.begin_transaction():
context.run_migrations() context.run_migrations()
+1
View File
@@ -39,6 +39,7 @@ unfixable = ["F401"]
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"scripts/*" = ["INP"] "scripts/*" = ["INP"]
"scripts/check_imports.py" = ["T201"]
"t/*" = ["ALL"] "t/*" = ["ALL"]
"*.lock" = ["ALL"] "*.lock" = ["ALL"]
"migrations/*" = ["INP", "D", "E501"] "migrations/*" = ["INP", "D", "E501"]
+3 -2
View File
@@ -4,8 +4,9 @@ import sys
HEAVY = ["aiogram", "pyrogram", "fastapi", "google.genai"] HEAVY = ["aiogram", "pyrogram", "fastapi", "google.genai"]
SERVICES = { SERVICES = {
"api": (["api.app"], {"fastapi"}), "api": (["api.app"], {"fastapi", "pyrogram"}),
"userbot": (["userbot.runner", "userbot.handlers"], {"pyrogram"}), "userbot": (["userbot.runner", "userbot.handlers"], {"pyrogram"}),
"watcher": (["watcher.runner"], {"pyrogram"}),
} }
PROBE = """ PROBE = """
import importlib, sys import importlib, sys
@@ -13,7 +14,7 @@ for module in sys.argv[1].split(","):
importlib.import_module(module) importlib.import_module(module)
print(" ".join(name for name in sys.argv[2:] if name in sys.modules)) 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]: 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.env import env
from utils.logging import logger, setup_logging from utils.logging import logger, setup_logging
<<<<<<< before updating
setup_logging() setup_logging()
@@ -19,9 +18,6 @@ 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:
+1 -1
View File
@@ -9,7 +9,7 @@ from pyrogram.errors import AuthTokenExpired, SessionPasswordNeeded
from pyrogram.qrlogin import QRLogin from pyrogram.qrlogin import QRLogin
from pyrogram.types import User from pyrogram.types import User
from userbot import PyroClient from userbot.modules.client import PyroClient
from utils.env import env from utils.env import env
LOGIN_TTL_SECONDS = 900 LOGIN_TTL_SECONDS = 900
+1 -1
View File
@@ -9,7 +9,7 @@ from pyrogram.errors import FloodWait, RPCError
from pyrogram.types import User from pyrogram.types import User
from api.login import LoginError, login_manager 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 import accounts
from utils.read.models import AccountView 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.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
-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 pyrogram.types import Message
from userbot import PyroClient
from userbot.modules.capture import repository from userbot.modules.capture import repository
from userbot.modules.capture.repository import CHANNEL_ID_THRESHOLD from userbot.modules.capture.repository import CHANNEL_ID_THRESHOLD
from userbot.modules.client import PyroClient
from utils.events import notify_bg_event from utils.events import notify_bg_event
+1 -1
View File
@@ -1,9 +1,9 @@
from pyrogram.types import Message from pyrogram.types import Message
from userbot import PyroClient
from userbot.modules.capture import repository from userbot.modules.capture import repository
from userbot.modules.capture.chat_meta import meta_from_chat from userbot.modules.capture.chat_meta import meta_from_chat
from userbot.modules.capture.message import sender_id 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.media import capture_media, media_unique_id, self_destruct_ttl
from userbot.modules.raw_json import raw_json from userbot.modules.raw_json import raw_json
from utils.events import notify_bg_event from utils.events import notify_bg_event
+1 -1
View File
@@ -1,8 +1,8 @@
from pyrogram.types import Message from pyrogram.types import Message
from userbot import PyroClient
from userbot.modules.capture import capture_message from userbot.modules.capture import capture_message
from userbot.modules.capture.chat_meta import meta_from_chat 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 import is_transcribable
from userbot.modules.stt.gate import safe_transcribe from userbot.modules.stt.gate import safe_transcribe
from utils.events import notify_bg_event 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 pyrogram.types import User
from userbot import PyroClient from userbot.modules.client import PyroClient
from userbot.modules.presence import repository from userbot.modules.presence import repository
from userbot.modules.raw_json import raw_json from userbot.modules.raw_json import raw_json
from utils.events import notify_bg_event from utils.events import notify_bg_event
+1 -1
View File
@@ -1,6 +1,6 @@
from pyrogram import raw from pyrogram import raw
from userbot import PyroClient from userbot.modules.client import PyroClient
HANDLES = (raw.types.UpdateContactsReset, raw.types.UpdateUser) HANDLES = (raw.types.UpdateContactsReset, raw.types.UpdateUser)
@@ -1,6 +1,6 @@
from pyrogram import raw from pyrogram import raw
from userbot import PyroClient from userbot.modules.client import PyroClient
HANDLES = ( HANDLES = (
raw.types.UpdateDialogFilter, raw.types.UpdateDialogFilter,
@@ -2,7 +2,6 @@ from collections.abc import Awaitable, Callable
from pyrogram import raw from pyrogram import raw
from userbot import PyroClient
from userbot.handlers.raw import ( from userbot.handlers.raw import (
contacts, contacts,
dialog_filters, dialog_filters,
@@ -12,6 +11,7 @@ from userbot.handlers.raw import (
read_receipts, read_receipts,
transcribed, transcribed,
) )
from userbot.modules.client import PyroClient
RawHandler = Callable[..., Awaitable[None]] RawHandler = Callable[..., Awaitable[None]]
+1 -1
View File
@@ -2,8 +2,8 @@ from dataclasses import replace
from pyrogram import raw from pyrogram import raw
from userbot import PyroClient
from userbot.modules.avatars import capture_avatar 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 import active_username, snapshot_from_user
from userbot.modules.profiles.parse import ProfileFields from userbot.modules.profiles.parse import ProfileFields
from userbot.modules.profiles.repository import get_peer, write_profile from userbot.modules.profiles.repository import get_peer, write_profile
@@ -1,8 +1,8 @@
from pyrogram import raw, utils from pyrogram import raw, utils
from userbot import PyroClient
from userbot.modules.capture import repository from userbot.modules.capture import repository
from userbot.modules.capture.chat_meta import meta_from_peer from userbot.modules.capture.chat_meta import meta_from_peer
from userbot.modules.client import PyroClient
from utils.events import notify_bg_event from utils.events import notify_bg_event
HANDLES = (raw.types.UpdateMessageReactions,) HANDLES = (raw.types.UpdateMessageReactions,)
@@ -2,8 +2,8 @@ from datetime import UTC, datetime
from pyrogram import raw, utils from pyrogram import raw, utils
from userbot import PyroClient
from userbot.modules.capture.chat_meta import meta_from_chat_id 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.raw_json import raw_json
from userbot.modules.read_receipts import repository as receipts from userbot.modules.read_receipts import repository as receipts
from userbot.modules.stt import repository from userbot.modules.stt import repository
@@ -2,7 +2,7 @@ from datetime import UTC, datetime
from pyrogram import raw, utils 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.raw_json import raw_json
from userbot.modules.read_receipts import repository from userbot.modules.read_receipts import repository
from utils.events import notify_bg_event from utils.events import notify_bg_event
@@ -1,7 +1,7 @@
from pyrogram import raw from pyrogram import raw
from userbot import PyroClient
from userbot.modules.capture.chat_meta import meta_from_peer from userbot.modules.capture.chat_meta import meta_from_peer
from userbot.modules.client import PyroClient
from userbot.modules.stt import repository from userbot.modules.stt import repository
HANDLES = (raw.types.UpdateTranscribedAudio,) HANDLES = (raw.types.UpdateTranscribedAudio,)
+1 -1
View File
@@ -1,6 +1,6 @@
from pyrogram.types import Story from pyrogram.types import Story
from userbot import PyroClient from userbot.modules.client import PyroClient
from userbot.modules.stories.service import save_story from userbot.modules.stories.service import save_story
@@ -41,9 +41,7 @@ class ChatMetaCache:
chat_id: int, chat_id: int,
meta: tuple[str | None, str | None], meta: tuple[str | None, str | None],
) -> bool: ) -> bool:
from userbot.modules.groups.repository import ( # noqa: PLC0415 from userbot.modules.groups.repository import get_latest_chat_meta # noqa: PLC0415
get_latest_chat_meta,
)
if chat_id not in self._cache: if chat_id not in self._cache:
self._cache[chat_id] = await get_latest_chat_meta(pool, account_id, chat_id) 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 import uvloop
from dependencies.container import container from dependencies.container import container
from userbot import PyroClient
from userbot.modules.capture import CaptureContext, build_capture_context from userbot.modules.capture import CaptureContext, build_capture_context
from userbot.modules.client import PyroClient
from userbot.modules.jobs import JobConsumer from userbot.modules.jobs import JobConsumer
from utils.env import env from utils.env import env
from utils.jobs import enqueue from utils.jobs import enqueue
+1 -20
View File
@@ -4,27 +4,7 @@ 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"
@@ -76,6 +56,7 @@ class LogSettings(BaseSettings):
level: str = "INFO" level: str = "INFO"
level_external: str = "WARNING" level_external: str = "WARNING"
show_time: bool = False show_time: bool = False
show_locals: bool = False
console_width: int = 150 console_width: int = 150
+1 -1
View File
@@ -13,10 +13,10 @@ from dataclasses import dataclass
from pyrogram import raw, types from pyrogram import raw, types
from pyrogram.errors import RPCError from pyrogram.errors import RPCError
from userbot import PyroClient
from userbot.modules.capture import capture_message from userbot.modules.capture import capture_message
from userbot.modules.capture.chat_meta import meta_from_chat from userbot.modules.capture.chat_meta import meta_from_chat
from userbot.modules.capture.context import CaptureContext from userbot.modules.capture.context import CaptureContext
from userbot.modules.client import PyroClient
from utils.logging import logger from utils.logging import logger
EARLIEST_PTS = 1 EARLIEST_PTS = 1
+1 -1
View File
@@ -11,9 +11,9 @@ from pyrogram.handlers import (
MessageHandler, MessageHandler,
) )
from userbot import PyroClient
from userbot.modules.capture import capture_message from userbot.modules.capture import capture_message
from userbot.modules.capture.chat_meta import meta_from_chat from userbot.modules.capture.chat_meta import meta_from_chat
from userbot.modules.client import PyroClient
from userbot.modules.jobs import JobConsumer from userbot.modules.jobs import JobConsumer
from utils.events import notify_bg_event from utils.events import notify_bg_event
from utils.jobs import enqueue from utils.jobs import enqueue