Compare commits

...
8 Commits
57 changed files with 184 additions and 130 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
+2 -1
View File
@@ -31,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`.
+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`.
+2 -21
View File
@@ -1,12 +1,6 @@
COMPOSE := cd .. && docker compose COMPOSE := cd .. && docker compose
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; }
.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 +10,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)
+10 -1
View File
@@ -20,10 +20,15 @@ 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_"))
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"},
) )
@@ -32,7 +37,11 @@ def run_migrations_offline() -> None:
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,
include_name=include_name,
)
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"]
+45
View File
@@ -0,0 +1,45 @@
import os
import subprocess
import sys
HEAVY = ["aiogram", "pyrogram", "fastapi", "google.genai"]
SERVICES = {
"api": (["api.app"], {"fastapi", "pyrogram"}),
"userbot": (["userbot.runner", "userbot.handlers"], {"pyrogram"}),
"watcher": (["watcher.runner"], {"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 = {**os.environ, "AUTH__TOKEN": "check"}
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())
+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
-3
View File
@@ -1,3 +0,0 @@
from userbot.modules.client import DEVICE_MODEL, DEVICE_MODEL_LIMIT, PyroClient
__all__ = ["DEVICE_MODEL", "DEVICE_MODEL_LIMIT", "PyroClient"]
+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
View File
@@ -56,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
+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
@@ -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
+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
+12 -12
View File
@@ -11,7 +11,7 @@
"uqr": "^0.1.3", "uqr": "^0.1.3",
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.14", "@biomejs/biome": "2.5.15",
"@sveltejs/adapter-auto": "^7.0.1", "@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/adapter-static": "^3.0.10", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.57.0", "@sveltejs/kit": "^2.57.0",
@@ -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",
}, },
}, },
@@ -39,23 +39,23 @@
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
"@biomejs/biome": ["@biomejs/biome@2.5.14", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.14", "@biomejs/cli-darwin-x64": "2.5.14", "@biomejs/cli-linux-arm64": "2.5.14", "@biomejs/cli-linux-arm64-musl": "2.5.14", "@biomejs/cli-linux-x64": "2.5.14", "@biomejs/cli-linux-x64-musl": "2.5.14", "@biomejs/cli-win32-arm64": "2.5.14", "@biomejs/cli-win32-x64": "2.5.14" }, "bin": { "biome": "bin/biome" } }, "sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw=="], "@biomejs/biome": ["@biomejs/biome@2.5.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.15", "@biomejs/cli-darwin-x64": "2.5.15", "@biomejs/cli-linux-arm64": "2.5.15", "@biomejs/cli-linux-arm64-musl": "2.5.15", "@biomejs/cli-linux-x64": "2.5.15", "@biomejs/cli-linux-x64-musl": "2.5.15", "@biomejs/cli-win32-arm64": "2.5.15", "@biomejs/cli-win32-x64": "2.5.15" }, "bin": { "biome": "bin/biome" } }, "sha512-WZTW4slm/pdkh92K6t/3aEN++44JD1PQ7squ7RCMLI1flHGl43DVOIGXvCjHiZgBMO1V4uYxoFNtqrIA4qGuIQ=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BZVzFhJ/mUvTLMYbc9x6el0o2Uv5zP7bACYyhFb6fSc1giroXm7PL0a5KqMJ9F+/BnkYRjagD/ROSDVOZ6eapg=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA=="], "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-V5Kw63V+fVGNFhFrizDxMbGXAzZCh8kYzJpy3GI6gTapWDg7bZK9oYC9CbVgQSEdTnupz0U5Efz8Ev+5vdBBXw=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q=="], "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-XaG7P7eeSLYETD3K9grfB0mQmpLQZjygdbsxekWBJAg4am79fGAtRpfRyNnTvXB2fZFNnJZG3nXm7HBat0+VUw=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ=="], "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-tGzZUTcJCV7tj5Adh/Gn6nR4MycqA3iohq2LEcrarAgRBkU0h0OKd2UzCRkOsOYOuUGaUJb4YmqRqyy2feGGSw=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA=="], "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.15", "", { "os": "linux", "cpu": "x64" }, "sha512-xE4iEW/3LqlYj9GFgGrFsFSH51dEEpUbYWBfeOv0q87WkUDxK2o/HhreSb7qMJqck70RVM6Lg96hgedcAYcOkA=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg=="], "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.15", "", { "os": "linux", "cpu": "x64" }, "sha512-IlxUcyxilVGPsE008x13pWdkTbU3nQpP2i9b5UrbOYj6goBkKsRX1XB7yJLcV+O1H9LBchIIm4adOaVLZBX0ZQ=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg=="], "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-2kPKzhNlm8C+Ru3GcO0Me2ODkCBLrVOUNuyi84RJyWDVKAA4+Kjj3jMjL938lF6BRfvltC4vB2nPY9SvN51Ovg=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.14", "", { "os": "win32", "cpu": "x64" }, "sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ=="], "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.15", "", { "os": "win32", "cpu": "x64" }, "sha512-yAzh4UqEImV6Hcy0zjUqNfJAsUhoD64FBQCdTG8Md/Z2wQeVP/ZmJ3XRYAs1g8J9fjTFyl2fCtc5FB5til47+g=="],
"@clack/core": ["@clack/core@1.5.1", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ=="], "@clack/core": ["@clack/core@1.5.1", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ=="],
@@ -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=="],
+2 -2
View File
@@ -20,7 +20,7 @@
"uqr": "^0.1.3" "uqr": "^0.1.3"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.14", "@biomejs/biome": "2.5.15",
"@sveltejs/adapter-auto": "^7.0.1", "@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/adapter-static": "^3.0.10", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.57.0", "@sveltejs/kit": "^2.57.0",
@@ -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"
} }
} }
@@ -151,7 +151,9 @@
</Button> </Button>
</div> </div>
<ContextMenu> <ContextMenu>
{#snippet children({ props })} {#snippet children({
props,
})}
<button <button
{...props} {...props}
aria-label="Открыть профиль" aria-label="Открыть профиль"
@@ -59,7 +59,9 @@
</script> </script>
<ContextMenu> <ContextMenu>
{#snippet children({ props })} {#snippet children({
props,
})}
<button <button
{...props} {...props}
class="Chat ListItem-button" class="Chat ListItem-button"
+15 -4
View File
@@ -27,7 +27,9 @@
} }
</script> </script>
{#snippet tree(items: EntityTreeNode[])} {#snippet tree(
items: EntityTreeNode[]
)}
{#each items as node, i (i)} {#each items as node, i (i)}
{#if node.kind === "text"} {#if node.kind === "text"}
{node.text} {node.text}
@@ -37,7 +39,9 @@
{/each} {/each}
{/snippet} {/snippet}
{#snippet entity(node: EntityNode)} {#snippet entity(
node: EntityNode
)}
{@const type = node.entity.type} {@const type = node.entity.type}
{#if type === "bold"} {#if type === "bold"}
<strong>{@render tree(node.children)}</strong> <strong>{@render tree(node.children)}</strong>
@@ -66,7 +70,10 @@
>{#if node.entity.language}<span class="pre-lang">{node.entity.language}</span>{/if}<code>{@render tree(node.children)}</code></pre> >{#if node.entity.language}<span class="pre-lang">{node.entity.language}</span>{/if}<code>{@render tree(node.children)}</code></pre>
{:else if type === "blockquote"} {:else if type === "blockquote"}
<blockquote class="blockquote">{@render tree(node.children)}</blockquote> <blockquote class="blockquote">{@render tree(node.children)}</blockquote>
{:else if type === "url" || type === "text_link" || type === "email" || type === "phone_number"} {:else if type === "url" ||
type === "text_link" ||
type === "email" ||
type === "phone_number"}
<a <a
class="link" class="link"
href={linkHref(node)} href={linkHref(node)}
@@ -75,7 +82,11 @@
class:own class:own
>{@render tree(node.children)}</a >{@render tree(node.children)}</a
> >
{:else if type === "mention" || type === "text_mention" || type === "hashtag" || type === "cashtag" || type === "bot_command"} {:else if type === "mention" ||
type === "text_mention" ||
type === "hashtag" ||
type === "cashtag" ||
type === "bot_command"}
<span class="link" class:own>{@render tree(node.children)}</span> <span class="link" class:own>{@render tree(node.children)}</span>
{:else} {:else}
{@render tree(node.children)} {@render tree(node.children)}
@@ -50,7 +50,9 @@
{:else if result.state === "ready"} {:else if result.state === "ready"}
<a <a
class="file" class="file"
download={isActiveContent(result.mime) ? `media_${version.id}` : undefined} download={isActiveContent(result.mime)
? `media_${version.id}`
: undefined}
href={result.url} href={result.url}
rel="noopener" rel="noopener"
target="_blank" target="_blank"
@@ -184,7 +184,9 @@
<div class="media-actions"> <div class="media-actions">
{#if canSave} {#if canSave}
<ContextMenu> <ContextMenu>
{#snippet children({ props })} {#snippet children({
props,
})}
<button <button
{...props} {...props}
aria-label="Скачать файл" aria-label="Скачать файл"
@@ -131,7 +131,9 @@
</script> </script>
<ContextMenu> <ContextMenu>
{#snippet children({ props })} {#snippet children({
props,
})}
<div <div
{...props} {...props}
class="Message" class="Message"
@@ -137,7 +137,9 @@
</script> </script>
<ContextMenu> <ContextMenu>
{#snippet children({ props })} {#snippet children({
props,
})}
<div {...props} class="message-media" use:visible={start}> <div {...props} class="message-media" use:visible={start}>
{#if message.is_self_destruct} {#if message.is_self_destruct}
<button class="media-chip self-destruct" onclick={onopen} type="button"> <button class="media-chip self-destruct" onclick={onopen} type="button">
+3 -1
View File
@@ -12,7 +12,9 @@
</script> </script>
<div class="Reactions" class:own> <div class="Reactions" class:own>
{#each reactions as reaction, index (reaction.custom_emoji_id ?? reaction.emoji ?? index)} {#each reactions as reaction, index (reaction.custom_emoji_id ??
reaction.emoji ??
index)}
<button <button
class="reaction" class="reaction"
{onclick} {onclick}
@@ -72,7 +72,9 @@
{/if} {/if}
<span class="badge"> <span class="badge">
<Icon name="microphone" size="0.875rem" /> <Icon name="microphone" size="0.875rem" />
{formatDuration(paused && currentTime <= POSTER_TIME ? duration : remaining)} {formatDuration(
paused && currentTime <= POSTER_TIME ? duration : remaining
)}
</span> </span>
</button> </button>
{#if transcript} {#if transcript}
@@ -172,7 +172,11 @@
}); });
</script> </script>
{#snippet card(record: PolicyRecord, title: string, onremove?: () => void)} {#snippet card(
record: PolicyRecord,
title: string,
onremove?: () => void
)}
<div class="card"> <div class="card">
<div class="card-head"> <div class="card-head">
<span class="card-title">{title}</span> <span class="card-title">{title}</span>
@@ -181,7 +181,9 @@
<div class="grid"> <div class="grid">
{#each items as item, index (item.id)} {#each items as item, index (item.id)}
<ContextMenu> <ContextMenu>
{#snippet children({ props })} {#snippet children({
props,
})}
<button <button
{...props} {...props}
class="tile" class="tile"
@@ -237,7 +239,9 @@
{#each items as item, index (item.id)} {#each items as item, index (item.id)}
<li> <li>
<ContextMenu> <ContextMenu>
{#snippet children({ props })} {#snippet children({
props,
})}
<button <button
{...props} {...props}
onclick={() => open(index)} onclick={() => open(index)}
@@ -178,7 +178,9 @@
{@const state = shareState(share)} {@const state = shareState(share)}
<li class="row" class:inactive={state !== "active"}> <li class="row" class:inactive={state !== "active"}>
<ContextMenu> <ContextMenu>
{#snippet children({ props })} {#snippet children({
props,
})}
<button <button
{...props} {...props}
class="head" class="head"
@@ -45,7 +45,9 @@
</script> </script>
<ContextMenu> <ContextMenu>
{#snippet children({ props })} {#snippet children({
props,
})}
<button <button
{...props} {...props}
class="tile" class="tile"
@@ -184,7 +184,7 @@
<Dialog.Root bind:open> <Dialog.Root bind:open>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay class="story-overlay" /> <Dialog.Overlay class="story-overlay" />
<Dialog.Content class="story-content {held ? 'held' : ''}"> <Dialog.Content class="story-content {held ? "held" : ""}">
<Dialog.Title class="story-a11y-title">Сторис</Dialog.Title> <Dialog.Title class="story-a11y-title">Сторис</Dialog.Title>
<div class="bars"> <div class="bars">
{#each items as item, i (item.story_id)} {#each items as item, i (item.story_id)}
@@ -226,7 +226,9 @@
{/if} {/if}
{#if canSave} {#if canSave}
<ContextMenu> <ContextMenu>
{#snippet children({ props })} {#snippet children({
props,
})}
<button <button
{...props} {...props}
aria-label="Скачать сторис" aria-label="Скачать сторис"
@@ -48,7 +48,9 @@
<ContextMenu.Root bind:open> <ContextMenu.Root bind:open>
<ContextMenu.Trigger> <ContextMenu.Trigger>
{#snippet child({ props })} {#snippet child({
props,
})}
{@render children({ props: triggerProps(props) })} {@render children({ props: triggerProps(props) })}
{/snippet} {/snippet}
</ContextMenu.Trigger> </ContextMenu.Trigger>