merged from gramsrv upstream

This commit is contained in:
onysd 2026-09-01 12:06:31 +03:00
parent 79c64ee916
commit 21a0856587
651 changed files with 54774 additions and 4590 deletions

100
deploy/docker/.env.example Normal file
View file

@ -0,0 +1,100 @@
# Generated automatically by scripts/new-docker-env.*. Keep the resulting .env
# private and never commit it.
COMPOSE_PROJECT_NAME=gramsrv-main
TELESRV_DEPLOYMENT_PROFILE=main-monolith-v1
TELESRV_IMAGE_PREFIX=ghcr.io/iamxvbaba/gramsrv
TELESRV_IMAGE_TAG=main
TELESRV_SERVER_BUILD_TARGET=server-test
TELESRV_LOG_LEVEL=info
# Build provenance for local builds.
TELESRV_BUILD_COMMIT=unknown
TELESRV_BUILD_BRANCH=main
TELESRV_BUILD_TREE_STATE=unknown
TELESRV_BUILD_DATE=unknown
# The host-network server and admin reach these dependencies through loopback.
POSTGRES_DB=telesrv_main
POSTGRES_USER=telesrv
POSTGRES_PASSWORD=CHANGEME
TELESRV_POSTGRES_HOST_PORT=15432
TELESRV_POSTGRES_DSN=postgres://telesrv:CHANGEME@127.0.0.1:15432/telesrv_main?sslmode=disable
TELESRV_REDIS_HOST_PORT=16379
TELESRV_REDIS_ADDR=127.0.0.1:16379
TELESRV_REDIS_PASSWORD=CHANGEME
# Independent random values generated for each deployment.
TELESRV_ADMIN_API_TOKEN=CHANGEME
TELESRV_ADMIN_UI_PASSWORD=CHANGEME
TELESRV_ADMIN_UI_TOKEN=
TELESRV_ADMIN_SESSION_KEY=CHANGEME
TELESRV_TURN_SECRET=CHANGEME
TELESRV_OTP_WEBHOOK_SECRET=CHANGEME
# Development delivery is accepted automatically on loopback. Internet-facing
# deployments must explicitly opt in or replace it with the webhook provider.
TELESRV_DEV_AUTH_CODE=12345
TELESRV_PHONE_CODE_DELIVERY_PROVIDER=development
TELESRV_ALLOW_INSECURE_DEVELOPMENT_AUTH=false
TELESRV_OTP_WEBHOOK_URL=
TELESRV_OTP_WEBHOOK_TIMEOUT=5s
# The published main test image deliberately contains a public test RSA key so
# fresh test clients share one fingerprint. Existing server_state is preserved.
# Build target "server" plus mode "generated" for a private deployment identity.
TELESRV_RSA_IDENTITY_MODE=test
# Must be a client-reachable IP address, never a DNS name.
TELESRV_ADVERTISE_IP=CHANGEME
TELESRV_SERVER_PORT=2398
TELESRV_DEFAULT_COUNTRY_CODE=CN
TELESRV_PUBLIC_BASE_URL=CHANGEME
TELESRV_PUBLIC_APP_SCHEME=telesrv
TELESRV_PUBLIC_WEB_BASE_URL=CHANGEME
TELESRV_PUBLIC_LINK_PORT=2401
# The monolith owns SFU, TURN, and RTMP. Host mode avoids one Docker mapping per
# TURN relay port. Bridge mode publishes a bounded 64-port compatibility range.
TELESRV_SERVER_HOST_NETWORK=true
TELESRV_SFU_ENABLE=true
TELESRV_SFU_UDP_PORT=12399
TELESRV_SFU_ADVERTISE_IP=CHANGEME
TELESRV_TURN_ENABLE=true
TELESRV_TURN_UDP_PORT=12400
TELESRV_TURN_ADVERTISE_IP=CHANGEME
TELESRV_TURN_RELAY_MIN_PORT=12500
TELESRV_TURN_RELAY_MAX_PORT=12999
TELESRV_TURN_BRIDGE_RELAY_MAX_PORT=12563
TELESRV_CALL_TURN_CREDENTIAL_TTL=6h
TELESRV_CALL_FORCE_RELAY=false
TELESRV_LIVESTREAM_ENABLE=true
TELESRV_LIVESTREAM_RTMP_PORT=2400
TELESRV_LIVESTREAM_RTMP_URL=CHANGEME
# Direct host listeners and their health-probe addresses.
TELESRV_PUBLIC_BIND_IP=127.0.0.1
TELESRV_PUBLIC_LISTEN_HOST=127.0.0.1
TELESRV_LOCAL_BIND_IP=127.0.0.1
TELESRV_LOCAL_LISTEN_HOST=127.0.0.1
TELESRV_SERVER_HEALTH_IP=127.0.0.1
TELESRV_SERVER_HEALTH_URL_HOST=127.0.0.1
TELESRV_ADMIN_API_PORT=2599
TELESRV_ADMIN_BIND_IP=127.0.0.1
TELESRV_ADMIN_LISTEN_HOST=127.0.0.1
TELESRV_ADMIN_HEALTH_IP=127.0.0.1
TELESRV_ADMIN_PORT=2600
# Durable media state. Switching an existing deployment between localfs and S3
# requires the explicit blob migration procedure.
TELESRV_BLOB_BACKEND=localfs
TELESRV_EXTERNAL_MEDIA_ENABLE=true
TELESRV_WEBPAGE_PREVIEW_ENABLE=true
TELESRV_S3_ENDPOINT=
TELESRV_S3_REGION=
TELESRV_S3_BUCKET=
TELESRV_S3_ACCESS_KEY_ID=
TELESRV_S3_SECRET_ACCESS_KEY=
TELESRV_S3_USE_SSL=true
TELESRV_S3_PATH_STYLE=false
TELESRV_S3_CREATE_BUCKET=false

View file

@ -0,0 +1 @@
LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBeEYvLzBNMCsvNVB6Z2ROYWdUWCtKK2RKZ3I3NVpDVHVpRzhpNHg3WXdtSkYramlPCkdDam03WDdCTENhTWMxK2hPWllETDMrR3ZsZS9BS3lrVzFxb3VhQ0pNVngvSCsybDhMRlhMZWxaMlBMYXdUYjgKQTdCbFRxV3pMM2RiNUJ1Z01OV3ppTDlUdWhSOEluMWJ3S1kwN1FWcFI5aW41empBc0FHTEJrK21HdDBEblZ5TQpmMVhvcDJsTENGTm1tMEY0eWtjQWVhTENDSVBiR1dkZGxpTFk4eEVFaEk0R08ybDFVM2taTXdJT2RPbkFHSkZ0CmdVQW9UZStGSFI2RjFzOWFkQ1ZaQjF0ZUwvaGY5UitXbWVrSnd5Z1Z6ME1ZRUg3eTZVNDlUNDUrL1c3T0Y2WDYKZzBXMGoxdVNTcnNZNHFON3R3eGJUYWQ5emRHWjd5cys5ditQdVFJREFRQUJBb0lCQUFqdXFPOHhkczBmU0xNKwpEdDdUdXVUTHcyODhDcElBa0EwS3FSYVZuNXh2NWVqMHk1blR1blZSRDY1WGJvb003b04xREY0THVmQk1nM2FmClk3WjRFRGFwVTdRNEZkdzQ3aFJkcks1ODc4WkxmYUhPUTNaVGZyZ3VGMUZ3WjNDZnhSQ1RsOS8vZStwNTVnK1gKamlYY0tZb2lkZUI3dlY5cUdIR3BFRTdRTHFrSUVpbk1FV05hQjh1dGN0SDdUWGRXYTRweWZJR2lQckhNTjJ6SApoRjQwSWI3bkpBNmtodHpzTkNEU0Q5WG5ibEVORW9kMUU1Z1JzalE5ZkdzaGRCdHBEc1hyTEJGTDdLTUREb1FtCmN6cnQvS3hsWk1wYnRPZno2dWE1ZUtFQkJUdE51dG1WY3AxcTl5K2NRcFBaem5ZaVJUTCtPSnpWZ2Z1SE9PLzAKZWEyai93RUNnWUVBNkk2ZitFTG84QVA3UTRXMHFzc3VYS3ZIRUYvUkVFZngyTHVlaDBsclVoVlIzSWdqYWtGYwpiVFJsTEVRSzhRTmJEQW1OTXVIckVabmptR3U3bnkzek45NHNsQy95Y0k1cGQ5aFFHVndPMjJlcjlvMitNOXcyCkpwQjRRZjhjOFNLL3BIUU45K2pPdEJ4VjJkcmJmZTBvVW9LRXRZeUQ1Y2J0akZUM3lmZUEyQ0VDZ1lFQTJDdW8KUzg0MWtWcHB4MUt3cEw0aTFmZ0dRUHlrYUtyRDRvR1pyWWd2MkZ4VzU1aS9xNUZjWUw2ZkUzN0phTTFwby9SQgp0bkhnMzVOYW5nL3l1Wlh6NkViNEswQ3lMKzhMdWhDSWI2UHhEdnF5Q1hia1hUd0hTVlJBbkdFTVNOM3phLzdGCkZDSkYxQUJXZzhqbEtSZEEvbFJ3cW84UDhaZ0JlQk1xcEtaaDVKa0NnWUJoQ0ltazI3NDN6MkY2dGdKQk5VL2QKNk9yQllVbHBJcXU5ZytOTWpZelREZ1EvSVNxdHZpSGppdllmOXpBZGlnbm1SdUg4ZGhsUUdjYkdKVVYrMEh4bwpOaktoampQNVZPS2ExODNzRnVZNEU5VERwamJUaXJHcGU2UkIzVUZsTjl1QXNjL1dQZlJwWUYxTjdpeWhLV0FtCnRVRE1RNW9ST09TTEpqVFJ0NHl5SVFLQmdRQ1RmeTVsRXYyd0FQMzk5K2o1YjVhN1luRjU5Q2lHRmtaMERiUDcKR05wMGlZVHVuMlhndmQxSFVhbWZGcnA4blBRQTM4L2FtZGN6RmdzVm9KSWdtVFdFZnJBa2F3OXA3M1NUNzJYNApydWJ6THBFK0xmWmh1MnpKVndpQzZ5RURzeFc5MFdkTmRwa29yMVpZczBIUmlNRmJCK2ljSitOY0dEaWdZb3VOCkxzM0t1UUtCZ1FDVEF4Vk03ek5QZ3NmZnVkWlA3Rk1ueTIwTE1SNitGUW83eFJ6NUVsWWFDVmdYTTFOSDRLL3IKeUZrT0NHaE9ONkkvTVVOQlB4V0xFdVpCUmZ5cmx5M2JwM2o3UjYvaExYNGZOeXc5QjNHUXVWeUNIRDJENHRvZAo2SjVWby9Fc2VxcGVlS2E2Q3YvWTJIVkIwa2ZHdzFWQ0MvZ00wMUw4dWVkM2hzcjJMRDJrQ3c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=

View file

@ -0,0 +1,8 @@
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAxF//0M0+/5PzgdNagTX+J+dJgr75ZCTuiG8i4x7YwmJF+jiOGCjm
7X7BLCaMc1+hOZYDL3+Gvle/AKykW1qouaCJMVx/H+2l8LFXLelZ2PLawTb8A7Bl
TqWzL3db5BugMNWziL9TuhR8In1bwKY07QVpR9in5zjAsAGLBk+mGt0DnVyMf1Xo
p2lLCFNmm0F4ykcAeaLCCIPbGWddliLY8xEEhI4GO2l1U3kZMwIOdOnAGJFtgUAo
Te+FHR6F1s9adCVZB1teL/hf9R+WmekJwygVz0MYEH7y6U49T45+/W7OF6X6g0W0
j1uSSrsY4qN7twxbTad9zdGZ7ys+9v+PuQIDAQAB
-----END RSA PUBLIC KEY-----

View file

@ -0,0 +1,63 @@
services:
server:
# Compatibility fallback for hosts without network_mode: host. The bounded
# TURN relay range is published 1:1 to avoid address translation mismatch.
network_mode: !reset null
environment:
TELESRV_LISTEN: 0.0.0.0:${TELESRV_SERVER_PORT:-2398}
TELESRV_PUBLIC_LINK_WEB_ADDR: 0.0.0.0:${TELESRV_PUBLIC_LINK_PORT:-2401}
TELESRV_POSTGRES_DSN: postgres://${POSTGRES_USER:-telesrv}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-telesrv_main}?sslmode=disable
TELESRV_REDIS_ADDR: redis:6379
TELESRV_ADMIN_API_ADDR: 0.0.0.0:${TELESRV_ADMIN_API_PORT:-2599}
TELESRV_TURN_RELAY_MAX_PORT: ${TELESRV_TURN_BRIDGE_RELAY_MAX_PORT:-12563}
TELESRV_SERVER_HEALTH_IP: 127.0.0.1
TELESRV_SERVER_HEALTH_URL_HOST: 127.0.0.1
ports:
- name: mtproto
target: ${TELESRV_SERVER_PORT:-2398}
published: "${TELESRV_SERVER_PORT:-2398}"
host_ip: ${TELESRV_PUBLIC_BIND_IP:-127.0.0.1}
protocol: tcp
- name: public-links
target: ${TELESRV_PUBLIC_LINK_PORT:-2401}
published: "${TELESRV_PUBLIC_LINK_PORT:-2401}"
host_ip: ${TELESRV_LOCAL_BIND_IP:-127.0.0.1}
protocol: tcp
- name: rtmp-ingest
target: ${TELESRV_LIVESTREAM_RTMP_PORT:-2400}
published: "${TELESRV_LIVESTREAM_RTMP_PORT:-2400}"
host_ip: ${TELESRV_PUBLIC_BIND_IP:-127.0.0.1}
protocol: tcp
- name: sfu-media
target: ${TELESRV_SFU_UDP_PORT:-12399}
published: "${TELESRV_SFU_UDP_PORT:-12399}"
host_ip: ${TELESRV_PUBLIC_BIND_IP:-127.0.0.1}
protocol: udp
- name: turn
target: ${TELESRV_TURN_UDP_PORT:-12400}
published: "${TELESRV_TURN_UDP_PORT:-12400}"
host_ip: ${TELESRV_PUBLIC_BIND_IP:-127.0.0.1}
protocol: udp
- "${TELESRV_PUBLIC_BIND_IP:-127.0.0.1}:${TELESRV_TURN_RELAY_MIN_PORT:-12500}-${TELESRV_TURN_BRIDGE_RELAY_MAX_PORT:-12563}:${TELESRV_TURN_RELAY_MIN_PORT:-12500}-${TELESRV_TURN_BRIDGE_RELAY_MAX_PORT:-12563}/udp"
networks:
- data
- control
- outbound
admin:
network_mode: !reset null
environment:
TELESRV_POSTGRES_DSN: postgres://${POSTGRES_USER:-telesrv}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-telesrv_main}?sslmode=disable
TELESRV_ADMIN_API_ADDR: server:${TELESRV_ADMIN_API_PORT:-2599}
TELESRV_ADMIN_UI_ADDR: 0.0.0.0:${TELESRV_ADMIN_PORT:-2600}
TELESRV_ADMIN_HEALTH_IP: 127.0.0.1
ports:
- name: admin-ui
target: ${TELESRV_ADMIN_PORT:-2600}
published: "${TELESRV_ADMIN_PORT:-2600}"
host_ip: ${TELESRV_ADMIN_BIND_IP:-127.0.0.1}
protocol: tcp
networks:
- data
- control
- admin_host_access

228
deploy/docker/compose.yaml Normal file
View file

@ -0,0 +1,228 @@
name: ${COMPOSE_PROJECT_NAME:-gramsrv-main}
x-build-args: &build-args
VCS_REF: ${TELESRV_BUILD_COMMIT:-unknown}
VCS_BRANCH: ${TELESRV_BUILD_BRANCH:-main}
VCS_TREE_STATE: ${TELESRV_BUILD_TREE_STATE:-unknown}
BUILD_DATE: ${TELESRV_BUILD_DATE:-unknown}
x-app: &app
init: true
restart: unless-stopped
read_only: true
user: "10001:10001"
cap_drop:
- ALL
pids_limit: 1024
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:rw,noexec,nosuid,nodev,size=128m
stop_grace_period: 1m
logging: &app-logging
driver: json-file
options:
max-size: 10m
max-file: "5"
services:
postgres:
image: postgres:17-alpine@sha256:979c4379dd698aba0b890599a6104e082035f98ef31d9b9291ec22f2b13059ca
environment:
POSTGRES_DB: ${POSTGRES_DB:-telesrv_main}
POSTGRES_USER: ${POSTGRES_USER:-telesrv}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in deploy/docker/.env}
POSTGRES_INITDB_ARGS: --data-checksums
TZ: UTC
command:
- postgres
- -c
- max_locks_per_transaction=512
- -c
- shared_preload_libraries=pg_stat_statements
- -c
- pg_stat_statements.track=all
- -c
- track_io_timing=on
shm_size: 256mb
volumes:
- postgres_data:/var/lib/postgresql/data
- ../postgres-init:/docker-entrypoint-initdb.d:ro
ports:
- name: server-host-postgres
target: 5432
published: "${TELESRV_POSTGRES_HOST_PORT:-15432}"
host_ip: 127.0.0.1
protocol: tcp
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""]
interval: 5s
timeout: 3s
retries: 20
start_period: 10s
restart: unless-stopped
stop_grace_period: 1m
logging: *app-logging
networks:
- data
- server_host_access
redis:
image: redis:7-alpine@sha256:8b81dd37ff027bec4e516d41acfbe9fe2460070dc6d4a4570a2ac5b9d59df065
environment:
REDIS_PASSWORD: ${TELESRV_REDIS_PASSWORD:?set TELESRV_REDIS_PASSWORD in deploy/docker/.env}
command:
- sh
- -ec
- exec redis-server --appendonly yes --appendfsync everysec --requirepass "$$REDIS_PASSWORD"
volumes:
- redis_data:/data
ports:
- name: server-host-redis
target: 6379
published: "${TELESRV_REDIS_HOST_PORT:-16379}"
host_ip: 127.0.0.1
protocol: tcp
healthcheck:
test: ["CMD-SHELL", "redis-cli --no-auth-warning -a \"$$REDIS_PASSWORD\" ping | grep -qx PONG"]
interval: 5s
timeout: 3s
retries: 20
start_period: 5s
restart: unless-stopped
stop_grace_period: 30s
logging: *app-logging
networks:
- data
- server_host_access
server:
<<: *app
image: ${TELESRV_IMAGE_PREFIX:-ghcr.io/iamxvbaba/gramsrv}/server:${TELESRV_IMAGE_TAG:-main}
build:
context: ../..
dockerfile: Dockerfile
target: ${TELESRV_SERVER_BUILD_TARGET:-server-test}
args: *build-args
environment:
TELESRV_LOG_LEVEL: ${TELESRV_LOG_LEVEL:-info}
TELESRV_LISTEN: ${TELESRV_PUBLIC_LISTEN_HOST:-127.0.0.1}:${TELESRV_SERVER_PORT:-2398}
TELESRV_ADVERTISE_IP: ${TELESRV_ADVERTISE_IP:?set TELESRV_ADVERTISE_IP in deploy/docker/.env}
TELESRV_DEFAULT_COUNTRY_CODE: ${TELESRV_DEFAULT_COUNTRY_CODE:-CN}
TELESRV_PUBLIC_BASE_URL: ${TELESRV_PUBLIC_BASE_URL:?set TELESRV_PUBLIC_BASE_URL in deploy/docker/.env}
TELESRV_PUBLIC_APP_SCHEME: ${TELESRV_PUBLIC_APP_SCHEME:-telesrv}
TELESRV_PUBLIC_WEB_BASE_URL: ${TELESRV_PUBLIC_WEB_BASE_URL:?set TELESRV_PUBLIC_WEB_BASE_URL in deploy/docker/.env}
TELESRV_PUBLIC_LINK_WEB_ADDR: ${TELESRV_LOCAL_LISTEN_HOST:-127.0.0.1}:${TELESRV_PUBLIC_LINK_PORT:-2401}
TELESRV_POSTGRES_DSN: ${TELESRV_POSTGRES_DSN:?set TELESRV_POSTGRES_DSN in deploy/docker/.env}
TELESRV_REDIS_ADDR: ${TELESRV_REDIS_ADDR:-127.0.0.1:16379}
TELESRV_REDIS_PASSWORD: ${TELESRV_REDIS_PASSWORD:?set TELESRV_REDIS_PASSWORD in deploy/docker/.env}
TELESRV_REDIS_DB: 0
TELESRV_ADMIN_API_ADDR: 127.0.0.1:${TELESRV_ADMIN_API_PORT:-2599}
TELESRV_ADMIN_API_TOKEN: ${TELESRV_ADMIN_API_TOKEN:?set TELESRV_ADMIN_API_TOKEN in deploy/docker/.env}
TELESRV_DEV_AUTH_CODE: ${TELESRV_DEV_AUTH_CODE:-12345}
TELESRV_PHONE_CODE_DELIVERY_PROVIDER: ${TELESRV_PHONE_CODE_DELIVERY_PROVIDER:-development}
TELESRV_OTP_WEBHOOK_URL: ${TELESRV_OTP_WEBHOOK_URL:-}
TELESRV_OTP_WEBHOOK_SECRET: ${TELESRV_OTP_WEBHOOK_SECRET:-}
TELESRV_OTP_WEBHOOK_TIMEOUT: ${TELESRV_OTP_WEBHOOK_TIMEOUT:-5s}
TELESRV_RSA_KEY: /var/lib/telesrv/server_rsa.pem
TELESRV_RSA_IDENTITY_MODE: ${TELESRV_RSA_IDENTITY_MODE:-test}
TELESRV_SFU_ENABLE: ${TELESRV_SFU_ENABLE:-true}
TELESRV_SFU_UDP_PORT: ${TELESRV_SFU_UDP_PORT:-12399}
TELESRV_SFU_ADVERTISE_IP: ${TELESRV_SFU_ADVERTISE_IP:?set TELESRV_SFU_ADVERTISE_IP in deploy/docker/.env}
TELESRV_TURN_ENABLE: ${TELESRV_TURN_ENABLE:-true}
TELESRV_TURN_UDP_PORT: ${TELESRV_TURN_UDP_PORT:-12400}
TELESRV_TURN_ADVERTISE_IP: ${TELESRV_TURN_ADVERTISE_IP:?set TELESRV_TURN_ADVERTISE_IP in deploy/docker/.env}
TELESRV_TURN_SECRET: ${TELESRV_TURN_SECRET:?set TELESRV_TURN_SECRET in deploy/docker/.env}
TELESRV_TURN_RELAY_MIN_PORT: ${TELESRV_TURN_RELAY_MIN_PORT:-12500}
TELESRV_TURN_RELAY_MAX_PORT: ${TELESRV_TURN_RELAY_MAX_PORT:-12999}
TELESRV_CALL_TURN_CREDENTIAL_TTL: ${TELESRV_CALL_TURN_CREDENTIAL_TTL:-6h}
TELESRV_CALL_FORCE_RELAY: ${TELESRV_CALL_FORCE_RELAY:-false}
TELESRV_LIVESTREAM_ENABLE: ${TELESRV_LIVESTREAM_ENABLE:-true}
TELESRV_LIVESTREAM_RTMP_ADDR: :${TELESRV_LIVESTREAM_RTMP_PORT:-2400}
TELESRV_LIVESTREAM_RTMP_URL: ${TELESRV_LIVESTREAM_RTMP_URL:?set TELESRV_LIVESTREAM_RTMP_URL in deploy/docker/.env}
TELESRV_LIVESTREAM_WORK_DIR: /var/lib/telesrv/livestream
TELESRV_LANGPACK_SEED_DIR: /usr/share/telesrv/langpack
TELESRV_BLOB_BACKEND: ${TELESRV_BLOB_BACKEND:-localfs}
TELESRV_BLOB_DIR: /var/lib/telesrv/blobs
TELESRV_BLOB_STAGING_DIR: /var/lib/telesrv/blob-staging
TELESRV_MAPTILE_CACHE_DIR: /var/lib/telesrv/maptiles
TELESRV_EXTERNAL_MEDIA_ENABLE: ${TELESRV_EXTERNAL_MEDIA_ENABLE:-true}
TELESRV_WEBPAGE_PREVIEW_ENABLE: ${TELESRV_WEBPAGE_PREVIEW_ENABLE:-true}
TELESRV_S3_ENDPOINT: ${TELESRV_S3_ENDPOINT:-}
TELESRV_S3_REGION: ${TELESRV_S3_REGION:-}
TELESRV_S3_BUCKET: ${TELESRV_S3_BUCKET:-}
TELESRV_S3_ACCESS_KEY_ID: ${TELESRV_S3_ACCESS_KEY_ID:-}
TELESRV_S3_SECRET_ACCESS_KEY: ${TELESRV_S3_SECRET_ACCESS_KEY:-}
TELESRV_S3_USE_SSL: ${TELESRV_S3_USE_SSL:-true}
TELESRV_S3_PATH_STYLE: ${TELESRV_S3_PATH_STYLE:-false}
TELESRV_S3_CREATE_BUCKET: ${TELESRV_S3_CREATE_BUCKET:-false}
TELESRV_SERVER_PORT: ${TELESRV_SERVER_PORT:-2398}
TELESRV_PUBLIC_LINK_PORT: ${TELESRV_PUBLIC_LINK_PORT:-2401}
TELESRV_SERVER_HEALTH_IP: ${TELESRV_SERVER_HEALTH_IP:-127.0.0.1}
TELESRV_SERVER_HEALTH_URL_HOST: ${TELESRV_SERVER_HEALTH_URL_HOST:-127.0.0.1}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- server_state:/var/lib/telesrv
network_mode: host
healthcheck:
test:
- CMD-SHELL
- >-
wget -q -O /dev/null "http://$${TELESRV_SERVER_HEALTH_URL_HOST}:$${TELESRV_PUBLIC_LINK_PORT}/healthz"
&& nc -z "$${TELESRV_SERVER_HEALTH_IP}" "$${TELESRV_SERVER_PORT}"
interval: 5s
timeout: 5s
retries: 114
start_period: 30s
admin:
<<: *app
image: ${TELESRV_IMAGE_PREFIX:-ghcr.io/iamxvbaba/gramsrv}/admin:${TELESRV_IMAGE_TAG:-main}
build:
context: ../..
dockerfile: Dockerfile
target: admin
args: *build-args
environment:
TELESRV_POSTGRES_DSN: ${TELESRV_POSTGRES_DSN:?set TELESRV_POSTGRES_DSN in deploy/docker/.env}
TELESRV_ADMIN_API_ADDR: 127.0.0.1:${TELESRV_ADMIN_API_PORT:-2599}
TELESRV_ADMIN_API_TOKEN: ${TELESRV_ADMIN_API_TOKEN:?set TELESRV_ADMIN_API_TOKEN in deploy/docker/.env}
TELESRV_ADMIN_UI_ADDR: ${TELESRV_ADMIN_LISTEN_HOST:-127.0.0.1}:${TELESRV_ADMIN_PORT:-2600}
TELESRV_ADMIN_UI_PASSWORD: ${TELESRV_ADMIN_UI_PASSWORD:?set TELESRV_ADMIN_UI_PASSWORD in deploy/docker/.env}
TELESRV_ADMIN_UI_TOKEN: ${TELESRV_ADMIN_UI_TOKEN:-}
TELESRV_ADMIN_SESSION_KEY: ${TELESRV_ADMIN_SESSION_KEY:?set TELESRV_ADMIN_SESSION_KEY in deploy/docker/.env}
TELESRV_BLOB_BACKEND: ${TELESRV_BLOB_BACKEND:-localfs}
TELESRV_BLOB_DIR: /var/lib/telesrv/blobs
TELESRV_BLOB_STAGING_DIR: /var/lib/telesrv/blob-staging
TELESRV_ADMIN_HEALTH_IP: ${TELESRV_ADMIN_HEALTH_IP:-127.0.0.1}
TELESRV_ADMIN_PORT: ${TELESRV_ADMIN_PORT:-2600}
depends_on:
server:
condition: service_healthy
volumes:
- server_state:/var/lib/telesrv:ro
network_mode: host
healthcheck:
test: ["CMD-SHELL", "nc -z \"$$TELESRV_ADMIN_HEALTH_IP\" \"$$TELESRV_ADMIN_PORT\""]
interval: 5s
timeout: 3s
retries: 12
start_period: 5s
networks:
data:
internal: true
server_host_access:
admin_host_access:
control:
internal: true
outbound:
volumes:
postgres_data:
redis_data:
server_state:

View file

@ -0,0 +1,99 @@
#!/bin/sh
set -eu
umask 077
command_name="${1##*/}"
require_secret() {
name="$1"
value="$(printenv "$name" 2>/dev/null || true)"
normalized="$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')"
case "$normalized" in
""|*changeme*|*change-me*|*replace-me*)
echo "telesrv: required secret $name is missing or still uses a placeholder" >&2
exit 64
;;
esac
}
require_value() {
name="$1"
value="$(printenv "$name" 2>/dev/null || true)"
normalized="$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')"
case "$normalized" in
""|*changeme*|*change-me*|*replace-me*)
echo "telesrv: required setting $name is missing or still uses a placeholder" >&2
exit 64
;;
esac
}
initialize_server_key() {
private_key="${TELESRV_RSA_KEY:-/var/lib/telesrv/server_rsa.pem}"
identity_mode="$(printf '%s' "${TELESRV_RSA_IDENTITY_MODE:-generated}" | tr '[:upper:]' '[:lower:]')"
embedded_private_key=/usr/share/telesrv/keys/test-server-rsa.pem.b64
key_dir=$(dirname -- "$private_key")
mkdir -p "$key_dir"
case "$identity_mode" in
generated) ;;
test)
if [ ! -f "$private_key" ]; then
if [ ! -r "$embedded_private_key" ]; then
echo "telesrv: test RSA identity requested, but this image does not contain the published test key; use the server-test target or set TELESRV_RSA_IDENTITY_MODE=generated" >&2
exit 66
fi
temporary_key="$private_key.tmp.$$"
trap 'rm -f "$temporary_key"' EXIT HUP INT TERM
base64 -d "$embedded_private_key" >"$temporary_key"
chmod 0600 "$temporary_key"
mv "$temporary_key" "$private_key"
trap - EXIT HUP INT TERM
echo "telesrv: WARNING using the published main test RSA identity; its private key is public" >&2
fi
;;
*)
echo "telesrv: TELESRV_RSA_IDENTITY_MODE must be test or generated" >&2
exit 64
;;
esac
if [ -f "$private_key" ]; then
if ! openssl rsa -in "$private_key" -check -noout >/dev/null 2>&1; then
echo "telesrv: $private_key is not a valid RSA private key" >&2
exit 65
fi
chmod 0600 "$private_key"
fi
}
case "$command_name" in
telesrv)
require_value TELESRV_ADVERTISE_IP
require_value TELESRV_PUBLIC_BASE_URL
require_value TELESRV_PUBLIC_WEB_BASE_URL
require_secret TELESRV_POSTGRES_DSN
require_secret TELESRV_REDIS_PASSWORD
require_secret TELESRV_ADMIN_API_TOKEN
require_secret TELESRV_TURN_SECRET
initialize_server_key
;;
telesrv-admin)
require_secret TELESRV_POSTGRES_DSN
require_secret TELESRV_ADMIN_API_TOKEN
require_secret TELESRV_ADMIN_SESSION_KEY
admin_password="$(printenv TELESRV_ADMIN_UI_PASSWORD 2>/dev/null || true)"
admin_token="$(printenv TELESRV_ADMIN_UI_TOKEN 2>/dev/null || true)"
if [ -n "$admin_password" ]; then
require_secret TELESRV_ADMIN_UI_PASSWORD
elif [ -n "$admin_token" ]; then
require_secret TELESRV_ADMIN_UI_TOKEN
else
echo "telesrv: TELESRV_ADMIN_UI_PASSWORD or TELESRV_ADMIN_UI_TOKEN is required" >&2
exit 64
fi
;;
esac
exec "$@"

View file

@ -2648,7 +2648,9 @@ CREATE TABLE public.secret_chats (
history_deleted boolean DEFAULT false NOT NULL,
random_id integer NOT NULL,
date integer NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL
created_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT secret_chats_nonzero_id CHECK ((chat_id <> 0)),
CONSTRAINT secret_chats_random_id_is_chat_id CHECK ((chat_id = random_id))
);
@ -4524,13 +4526,6 @@ CREATE INDEX upload_parts_object_key_idx ON public.upload_parts USING btree (obj
CREATE UNIQUE INDEX uq_emq_dedup ON public.encrypted_message_queue USING btree (receiver_auth_key_id, chat_id, random_id);
--
-- Name: uq_secret_chats_admin_random; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX uq_secret_chats_admin_random ON public.secret_chats USING btree (admin_auth_key_id, random_id) WHERE (state <> 'discarded'::text);
--
-- Name: user_channel_member_index_admined_public_idx; Type: INDEX; Schema: public; Owner: -
--

View file

@ -0,0 +1,2 @@
DROP TABLE IF EXISTS broadcast_recipients;
DROP TABLE IF EXISTS broadcasts;

View file

@ -0,0 +1,54 @@
CREATE TABLE broadcasts (
id bigserial PRIMARY KEY,
message text NOT NULL CHECK (message <> '' AND octet_length(message) <= 4096),
target_mode varchar(16) NOT NULL CHECK (target_mode IN ('all', 'selected')),
snapshot_max_user_id bigint NOT NULL DEFAULT 0,
enumeration_cursor_user_id bigint NOT NULL DEFAULT 0,
enumeration_done boolean NOT NULL DEFAULT false,
target_count bigint NOT NULL DEFAULT 0 CHECK (target_count >= 0),
materialized_count bigint NOT NULL DEFAULT 0 CHECK (materialized_count >= 0),
sent_count bigint NOT NULL DEFAULT 0 CHECK (sent_count >= 0),
failed_count bigint NOT NULL DEFAULT 0 CHECK (failed_count >= 0),
created_by varchar(128) NOT NULL DEFAULT '',
created_at timestamptz NOT NULL DEFAULT now(),
CHECK (enumeration_cursor_user_id >= 0 AND enumeration_cursor_user_id <= snapshot_max_user_id),
CHECK (sent_count + failed_count <= materialized_count)
);
CREATE TABLE broadcast_recipients (
id bigserial PRIMARY KEY,
broadcast_id bigint NOT NULL REFERENCES broadcasts(id) ON DELETE CASCADE,
user_id bigint NOT NULL,
status varchar(16) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'processing', 'sent', 'failed')),
attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
next_attempt_at timestamptz NOT NULL DEFAULT now(),
lease_token varchar(64) NOT NULL DEFAULT '',
lease_until timestamptz,
last_error varchar(500) NOT NULL DEFAULT '',
private_message_id bigint NOT NULL DEFAULT 0,
message_box_id integer NOT NULL DEFAULT 0,
pts integer NOT NULL DEFAULT 0,
sent_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (broadcast_id, user_id),
CHECK (
(status = 'sent' AND private_message_id > 0 AND message_box_id > 0 AND pts > 0 AND sent_at IS NOT NULL)
OR
(status <> 'sent' AND private_message_id = 0 AND message_box_id = 0 AND pts = 0 AND sent_at IS NULL)
),
CHECK (
(status = 'processing' AND lease_token <> '' AND lease_until IS NOT NULL)
OR
(status <> 'processing' AND lease_token = '' AND lease_until IS NULL)
)
);
CREATE INDEX broadcasts_enumeration_idx ON broadcasts (id)
WHERE target_mode = 'all' AND NOT enumeration_done;
CREATE INDEX broadcast_recipients_pending_idx ON broadcast_recipients (next_attempt_at, id)
WHERE status = 'pending';
CREATE INDEX broadcast_recipients_processing_idx ON broadcast_recipients (lease_until, id)
WHERE status = 'processing';
CREATE INDEX broadcast_recipients_broadcast_idx ON broadcast_recipients (broadcast_id, id);

View file

@ -0,0 +1,3 @@
DROP INDEX IF EXISTS public.channel_messages_public_forward_source_seek_idx;
DROP INDEX IF EXISTS public.channel_members_stats_period_idx;
DROP INDEX IF EXISTS public.channel_message_viewers_stats_date_idx;

View file

@ -0,0 +1,18 @@
-- Bounded stats reads: event-time viewers, membership snapshots, and exact
-- public-forward seek pagination by the durable MessageForward JSON shape.
CREATE INDEX channel_message_viewers_stats_date_idx
ON public.channel_message_viewers (channel_id, viewed_at, viewer_user_id, message_id);
CREATE INDEX channel_members_stats_period_idx
ON public.channel_members (channel_id, joined_at, left_at, user_id);
CREATE INDEX channel_messages_public_forward_source_seek_idx
ON public.channel_messages (
(fwd_from #>> '{From,Type}'),
(fwd_from #>> '{From,ID}'),
(fwd_from #>> '{ChannelPost}'),
message_date DESC,
channel_id ASC,
id DESC
)
WHERE NOT deleted AND fwd_from <> '{}'::jsonb;

View file

@ -0,0 +1,3 @@
ALTER TABLE broadcasts
DROP CONSTRAINT IF EXISTS broadcasts_entities_array_check,
DROP COLUMN IF EXISTS entities;

View file

@ -0,0 +1,6 @@
ALTER TABLE broadcasts
ADD COLUMN entities jsonb NOT NULL DEFAULT '[]'::jsonb;
ALTER TABLE broadcasts
ADD CONSTRAINT broadcasts_entities_array_check
CHECK (jsonb_typeof(entities) = 'array');

View file

@ -0,0 +1,112 @@
CREATE OR REPLACE FUNCTION public.telesrv_notify_user_base_read_model() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
changed_id BIGINT;
projection_changed BOOLEAN;
BEGIN
IF TG_OP = 'DELETE' THEN
changed_id := OLD.id;
projection_changed := true;
ELSE
changed_id := NEW.id;
IF TG_OP = 'INSERT' THEN
projection_changed := true;
ELSE
projection_changed :=
OLD.access_hash IS DISTINCT FROM NEW.access_hash OR
OLD.phone IS DISTINCT FROM NEW.phone OR
OLD.first_name IS DISTINCT FROM NEW.first_name OR
OLD.last_name IS DISTINCT FROM NEW.last_name OR
OLD.username IS DISTINCT FROM NEW.username OR
OLD.country_code IS DISTINCT FROM NEW.country_code OR
OLD.verified IS DISTINCT FROM NEW.verified OR
OLD.support IS DISTINCT FROM NEW.support OR
OLD.about IS DISTINCT FROM NEW.about OR
OLD.default_history_ttl_period IS DISTINCT FROM NEW.default_history_ttl_period OR
OLD.is_bot IS DISTINCT FROM NEW.is_bot OR
OLD.bot_info_version IS DISTINCT FROM NEW.bot_info_version OR
OLD.premium_expires_at IS DISTINCT FROM NEW.premium_expires_at OR
OLD.emoji_status_document_id IS DISTINCT FROM NEW.emoji_status_document_id OR
OLD.emoji_status_until IS DISTINCT FROM NEW.emoji_status_until OR
OLD.color_set IS DISTINCT FROM NEW.color_set OR
OLD.color IS DISTINCT FROM NEW.color OR
OLD.color_background_emoji_id IS DISTINCT FROM NEW.color_background_emoji_id OR
OLD.profile_color_set IS DISTINCT FROM NEW.profile_color_set OR
OLD.profile_color IS DISTINCT FROM NEW.profile_color OR
OLD.profile_color_background_emoji_id IS DISTINCT FROM NEW.profile_color_background_emoji_id;
END IF;
END IF;
IF projection_changed THEN
PERFORM telesrv_bump_read_model_version('user_base', changed_id, 'user', changed_id);
IF TG_OP = 'INSERT' THEN
PERFORM telesrv_bump_read_model_version('contact_account', changed_id, 'user', changed_id);
END IF;
PERFORM telesrv_bump_read_model_version('contact_account', c.user_id, 'user', c.user_id)
FROM contacts c
WHERE c.contact_user_id = changed_id;
PERFORM telesrv_bump_private_dialog_light_for_user(changed_id);
END IF;
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_user_channel_participants_read_model() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
changed_id BIGINT;
old_id BIGINT;
projection_changed BOOLEAN;
BEGIN
IF TG_OP = 'DELETE' THEN
changed_id := OLD.id;
projection_changed := true;
ELSE
changed_id := NEW.id;
IF TG_OP = 'INSERT' THEN
projection_changed := true;
PERFORM telesrv_bump_read_model_version('contact_account', changed_id, 'user', changed_id);
ELSE
old_id := OLD.id;
projection_changed :=
OLD.access_hash IS DISTINCT FROM NEW.access_hash OR
OLD.phone IS DISTINCT FROM NEW.phone OR
OLD.first_name IS DISTINCT FROM NEW.first_name OR
OLD.last_name IS DISTINCT FROM NEW.last_name OR
OLD.username IS DISTINCT FROM NEW.username OR
OLD.country_code IS DISTINCT FROM NEW.country_code OR
OLD.verified IS DISTINCT FROM NEW.verified OR
OLD.support IS DISTINCT FROM NEW.support OR
OLD.about IS DISTINCT FROM NEW.about OR
OLD.default_history_ttl_period IS DISTINCT FROM NEW.default_history_ttl_period OR
OLD.is_bot IS DISTINCT FROM NEW.is_bot OR
OLD.bot_info_version IS DISTINCT FROM NEW.bot_info_version OR
OLD.premium_expires_at IS DISTINCT FROM NEW.premium_expires_at OR
OLD.emoji_status_document_id IS DISTINCT FROM NEW.emoji_status_document_id OR
OLD.emoji_status_until IS DISTINCT FROM NEW.emoji_status_until OR
OLD.color_set IS DISTINCT FROM NEW.color_set OR
OLD.color IS DISTINCT FROM NEW.color OR
OLD.color_background_emoji_id IS DISTINCT FROM NEW.color_background_emoji_id OR
OLD.profile_color_set IS DISTINCT FROM NEW.profile_color_set OR
OLD.profile_color IS DISTINCT FROM NEW.profile_color OR
OLD.profile_color_background_emoji_id IS DISTINCT FROM NEW.profile_color_background_emoji_id;
IF old_id IS DISTINCT FROM changed_id THEN
PERFORM telesrv_bump_channel_participants_for_user(old_id);
END IF;
END IF;
END IF;
IF projection_changed THEN
PERFORM telesrv_bump_channel_participants_for_user(changed_id);
END IF;
RETURN NULL;
END;
$$;
DROP TRIGGER IF EXISTS users_release_collectible_phone_on_soft_delete ON public.users;
CREATE TRIGGER users_release_collectible_phone_on_soft_delete
BEFORE UPDATE OF deleted_at ON public.users
FOR EACH ROW WHEN (OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL)
EXECUTE FUNCTION public.release_soft_deleted_user_collectible_phone();

View file

@ -0,0 +1,129 @@
-- Human account deletion is a logical user tombstone. The deleted user keeps
-- all relationship/history rows, so the users UPDATE must not fan out through
-- every reverse contact, dialog and channel membership. A dedicated event
-- invalidates the base-user and RPC projection caches as one coarse boundary.
-- Collectible phone ownership is an account asset, not the editable users.phone
-- identity field. Logical deletion preserves it; physical user deletion keeps
-- the separate 0171 release trigger.
DROP TRIGGER IF EXISTS users_release_collectible_phone_on_soft_delete ON public.users;
CREATE OR REPLACE FUNCTION public.telesrv_notify_user_base_read_model() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
changed_id BIGINT;
projection_changed BOOLEAN;
BEGIN
IF TG_OP = 'UPDATE'
AND OLD.deleted_at IS NULL
AND NEW.deleted_at IS NOT NULL THEN
PERFORM telesrv_bump_read_model_version('user_deleted', NEW.id, 'user', NEW.id);
RETURN NULL;
END IF;
IF TG_OP = 'DELETE' THEN
changed_id := OLD.id;
projection_changed := true;
ELSE
changed_id := NEW.id;
IF TG_OP = 'INSERT' THEN
projection_changed := true;
ELSE
projection_changed :=
OLD.access_hash IS DISTINCT FROM NEW.access_hash OR
OLD.phone IS DISTINCT FROM NEW.phone OR
OLD.first_name IS DISTINCT FROM NEW.first_name OR
OLD.last_name IS DISTINCT FROM NEW.last_name OR
OLD.username IS DISTINCT FROM NEW.username OR
OLD.country_code IS DISTINCT FROM NEW.country_code OR
OLD.verified IS DISTINCT FROM NEW.verified OR
OLD.support IS DISTINCT FROM NEW.support OR
OLD.about IS DISTINCT FROM NEW.about OR
OLD.default_history_ttl_period IS DISTINCT FROM NEW.default_history_ttl_period OR
OLD.is_bot IS DISTINCT FROM NEW.is_bot OR
OLD.bot_info_version IS DISTINCT FROM NEW.bot_info_version OR
OLD.premium_expires_at IS DISTINCT FROM NEW.premium_expires_at OR
OLD.emoji_status_document_id IS DISTINCT FROM NEW.emoji_status_document_id OR
OLD.emoji_status_until IS DISTINCT FROM NEW.emoji_status_until OR
OLD.color_set IS DISTINCT FROM NEW.color_set OR
OLD.color IS DISTINCT FROM NEW.color OR
OLD.color_background_emoji_id IS DISTINCT FROM NEW.color_background_emoji_id OR
OLD.profile_color_set IS DISTINCT FROM NEW.profile_color_set OR
OLD.profile_color IS DISTINCT FROM NEW.profile_color OR
OLD.profile_color_background_emoji_id IS DISTINCT FROM NEW.profile_color_background_emoji_id;
END IF;
END IF;
IF projection_changed THEN
PERFORM telesrv_bump_read_model_version('user_base', changed_id, 'user', changed_id);
IF TG_OP = 'INSERT' THEN
PERFORM telesrv_bump_read_model_version('contact_account', changed_id, 'user', changed_id);
END IF;
PERFORM telesrv_bump_read_model_version('contact_account', c.user_id, 'user', c.user_id)
FROM contacts c
WHERE c.contact_user_id = changed_id;
PERFORM telesrv_bump_private_dialog_light_for_user(changed_id);
END IF;
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_user_channel_participants_read_model() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
changed_id BIGINT;
old_id BIGINT;
projection_changed BOOLEAN;
BEGIN
IF TG_OP = 'UPDATE'
AND OLD.deleted_at IS NULL
AND NEW.deleted_at IS NOT NULL THEN
RETURN NULL;
END IF;
IF TG_OP = 'DELETE' THEN
changed_id := OLD.id;
projection_changed := true;
ELSE
changed_id := NEW.id;
IF TG_OP = 'INSERT' THEN
projection_changed := true;
PERFORM telesrv_bump_read_model_version('contact_account', changed_id, 'user', changed_id);
ELSE
old_id := OLD.id;
projection_changed :=
OLD.access_hash IS DISTINCT FROM NEW.access_hash OR
OLD.phone IS DISTINCT FROM NEW.phone OR
OLD.first_name IS DISTINCT FROM NEW.first_name OR
OLD.last_name IS DISTINCT FROM NEW.last_name OR
OLD.username IS DISTINCT FROM NEW.username OR
OLD.country_code IS DISTINCT FROM NEW.country_code OR
OLD.verified IS DISTINCT FROM NEW.verified OR
OLD.support IS DISTINCT FROM NEW.support OR
OLD.about IS DISTINCT FROM NEW.about OR
OLD.default_history_ttl_period IS DISTINCT FROM NEW.default_history_ttl_period OR
OLD.is_bot IS DISTINCT FROM NEW.is_bot OR
OLD.bot_info_version IS DISTINCT FROM NEW.bot_info_version OR
OLD.premium_expires_at IS DISTINCT FROM NEW.premium_expires_at OR
OLD.emoji_status_document_id IS DISTINCT FROM NEW.emoji_status_document_id OR
OLD.emoji_status_until IS DISTINCT FROM NEW.emoji_status_until OR
OLD.color_set IS DISTINCT FROM NEW.color_set OR
OLD.color IS DISTINCT FROM NEW.color OR
OLD.color_background_emoji_id IS DISTINCT FROM NEW.color_background_emoji_id OR
OLD.profile_color_set IS DISTINCT FROM NEW.profile_color_set OR
OLD.profile_color IS DISTINCT FROM NEW.profile_color OR
OLD.profile_color_background_emoji_id IS DISTINCT FROM NEW.profile_color_background_emoji_id;
IF old_id IS DISTINCT FROM changed_id THEN
PERFORM telesrv_bump_channel_participants_for_user(old_id);
END IF;
END IF;
END IF;
IF projection_changed THEN
PERFORM telesrv_bump_channel_participants_for_user(changed_id);
END IF;
RETURN NULL;
END;
$$;

View file

@ -0,0 +1,12 @@
DROP TRIGGER IF EXISTS channel_messages_suggested_post_lifecycle_wakeup ON public.channel_messages;
DROP FUNCTION IF EXISTS public.telesrv_wake_suggested_post_lifecycle_on_delete();
DROP TABLE IF EXISTS public.suggested_post_lifecycle_wakeups;
DROP INDEX IF EXISTS public.suggested_post_approvals_published_message_idx;
DROP INDEX IF EXISTS public.suggested_post_approvals_retry_idx;
ALTER TABLE public.suggested_post_approvals
DROP CONSTRAINT IF EXISTS suggested_post_approvals_retry_shape_check,
DROP COLUMN IF EXISTS last_lifecycle_error,
DROP COLUMN IF EXISTS next_attempt_at,
DROP COLUMN IF EXISTS lifecycle_attempts;

View file

@ -0,0 +1,94 @@
-- Isolate poisoned suggested-post lifecycle rows from the global due queue.
-- A failed aggregate remains durable and retryable, but receives a bounded
-- backoff so it cannot monopolize every one-second dispatcher pass.
ALTER TABLE public.suggested_post_approvals
ADD COLUMN lifecycle_attempts integer NOT NULL DEFAULT 0,
ADD COLUMN next_attempt_at integer NOT NULL DEFAULT 0,
ADD COLUMN last_lifecycle_error text NOT NULL DEFAULT '';
ALTER TABLE public.suggested_post_approvals
ADD CONSTRAINT suggested_post_approvals_retry_shape_check CHECK (
lifecycle_attempts BETWEEN 0 AND 1000000 AND
next_attempt_at >= 0 AND
octet_length(last_lifecycle_error) <= 4096
) NOT VALID;
ALTER TABLE public.suggested_post_approvals
VALIDATE CONSTRAINT suggested_post_approvals_retry_shape_check;
-- next_attempt_at is the queue's next eligible time, not merely a retry flag.
-- Healthy active rows point at their publish/settlement deadline; only an
-- explicit message-deletion wakeup or an already-due row is <= worker now.
UPDATE public.suggested_post_approvals
SET next_attempt_at = CASE state
WHEN 'scheduled' THEN schedule_date
WHEN 'published' THEN settlement_due
ELSE 0
END
WHERE state IN ('scheduled','published');
CREATE INDEX suggested_post_approvals_retry_idx
ON public.suggested_post_approvals(next_attempt_at,monoforum_id,suggestion_message_id)
WHERE state IN ('scheduled','published');
CREATE INDEX suggested_post_approvals_published_message_idx
ON public.suggested_post_approvals(parent_channel_id,published_message_id)
INCLUDE (monoforum_id,suggestion_message_id,next_attempt_at)
WHERE state='published';
-- A separate wakeup table avoids a message-row -> approval-row trigger lock
-- that would invert the worker's approval-row -> message-row order. It only
-- contains active suggested-post aggregates and is drained by atomic claim.
CREATE TABLE public.suggested_post_lifecycle_wakeups (
monoforum_id bigint NOT NULL,
suggestion_message_id integer NOT NULL,
created_at integer NOT NULL,
CONSTRAINT suggested_post_lifecycle_wakeups_pkey
PRIMARY KEY (monoforum_id,suggestion_message_id),
CONSTRAINT suggested_post_lifecycle_wakeups_shape_check
CHECK (monoforum_id>0 AND suggestion_message_id>0 AND created_at>=0)
);
CREATE INDEX suggested_post_lifecycle_wakeups_due_idx
ON public.suggested_post_lifecycle_wakeups(created_at,monoforum_id,suggestion_message_id);
-- Preserve deletions that predate this migration without making the recurring
-- worker rescan global channel tombstone history.
INSERT INTO public.suggested_post_lifecycle_wakeups(monoforum_id,suggestion_message_id,created_at)
SELECT a.monoforum_id,a.suggestion_message_id,0
FROM public.suggested_post_approvals a
WHERE (a.state='scheduled' AND EXISTS (
SELECT 1 FROM public.channel_messages m
WHERE m.channel_id=a.monoforum_id AND m.id=a.suggestion_message_id AND m.deleted))
OR (a.state='published' AND EXISTS (
SELECT 1 FROM public.channel_messages m
WHERE m.channel_id=a.parent_channel_id AND m.id=a.published_message_id AND m.deleted))
ON CONFLICT DO NOTHING;
-- Deletion is a durable lifecycle wakeup. Drive the lookup from the exact
-- message being tombstoned and only insert a small queue fact; never rescan
-- global channel tombstones from the one-second worker.
CREATE OR REPLACE FUNCTION public.telesrv_wake_suggested_post_lifecycle_on_delete()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO public.suggested_post_lifecycle_wakeups(monoforum_id,suggestion_message_id,created_at)
SELECT monoforum_id,suggestion_message_id,EXTRACT(EPOCH FROM clock_timestamp())::integer
FROM public.suggested_post_approvals
WHERE state='scheduled' AND monoforum_id=NEW.channel_id AND suggestion_message_id=NEW.id
ON CONFLICT DO NOTHING;
INSERT INTO public.suggested_post_lifecycle_wakeups(monoforum_id,suggestion_message_id,created_at)
SELECT monoforum_id,suggestion_message_id,EXTRACT(EPOCH FROM clock_timestamp())::integer
FROM public.suggested_post_approvals
WHERE state='published' AND parent_channel_id=NEW.channel_id AND published_message_id=NEW.id
ON CONFLICT DO NOTHING;
RETURN NEW;
END
$$;
CREATE TRIGGER channel_messages_suggested_post_lifecycle_wakeup
AFTER UPDATE OF deleted ON public.channel_messages
FOR EACH ROW
WHEN (NEW.deleted AND NOT OLD.deleted)
EXECUTE FUNCTION public.telesrv_wake_suggested_post_lifecycle_on_delete();

View file

@ -0,0 +1,5 @@
DROP INDEX IF EXISTS public.sticker_sets_system_key_idx;
CREATE INDEX sticker_sets_system_key_idx
ON public.sticker_sets USING btree (system_key)
WHERE system_key <> ''::text;

View file

@ -0,0 +1,25 @@
-- system_key is the constructor-routing identity for global sticker sets. More
-- than one row makes messages.getStickerSet/inputStickerSet* nondeterministic
-- and can alternate clients between different catalogs after a restart.
-- StatusPack is now imported from the official export. Preserve the historical
-- synthesized set as an addressable short-name catalog, but only remove its
-- routing key when another real default-status set already owns that key.
UPDATE public.sticker_sets AS synthesized
SET system_key = '', updated_at = now()
WHERE synthesized.id = 7777000000000001
AND synthesized.system_key = 'emoji_default_statuses'
AND EXISTS (
SELECT 1
FROM public.sticker_sets AS real_set
WHERE real_set.system_key = synthesized.system_key
AND real_set.id <> synthesized.id
);
DROP INDEX IF EXISTS public.sticker_sets_system_key_idx;
-- Fail closed on any other duplicate instead of making the read path choose an
-- arbitrary row. Empty keys are ordinary/non-system sets and are not unique.
CREATE UNIQUE INDEX sticker_sets_system_key_idx
ON public.sticker_sets USING btree (system_key)
WHERE system_key <> ''::text;

View file

@ -0,0 +1,2 @@
DROP TABLE IF EXISTS welcome_messages;
DROP TABLE IF EXISTS welcome_message_peers;

View file

@ -0,0 +1,31 @@
CREATE TABLE welcome_message_peers (
channel_id bigint PRIMARY KEY REFERENCES channels(id) ON DELETE CASCADE,
next_id integer NOT NULL DEFAULT 1,
revision bigint NOT NULL DEFAULT 1,
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT welcome_message_peers_shape CHECK (
channel_id > 0 AND next_id > 0 AND next_id <= 2147483647 AND revision > 0
)
);
CREATE TABLE welcome_messages (
channel_id bigint NOT NULL REFERENCES welcome_message_peers(channel_id) ON DELETE CASCADE,
id integer NOT NULL,
creator_user_id bigint NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
date integer NOT NULL,
edit_date integer NOT NULL DEFAULT 0,
random_id bigint NOT NULL,
content jsonb NOT NULL,
create_fingerprint bytea NOT NULL,
version bigint NOT NULL DEFAULT 1,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (channel_id, id),
UNIQUE (channel_id, creator_user_id, random_id),
CONSTRAINT welcome_messages_shape CHECK (
channel_id > 0 AND id > 0 AND creator_user_id > 0 AND date > 0 AND
edit_date >= 0 AND (edit_date = 0 OR edit_date >= date) AND random_id <> 0 AND
jsonb_typeof(content) = 'object' AND pg_column_size(content) <= 4194304 AND
octet_length(create_fingerprint) = 32 AND version > 0
)
);

View file

@ -0,0 +1,2 @@
DROP TABLE IF EXISTS welcome_message_deliveries;
DROP SEQUENCE IF EXISTS welcome_message_join_event_id_seq;

View file

@ -0,0 +1,37 @@
CREATE SEQUENCE welcome_message_join_event_id_seq AS bigint;
CREATE TABLE welcome_message_deliveries (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
join_event_id bigint NOT NULL,
channel_id bigint NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
target_user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE,
template_id integer NOT NULL,
ephemeral_id integer GENERATED ALWAYS AS (((id - 1) % 2147483646 + 1)::integer) STORED,
joined_at integer NOT NULL,
content jsonb NOT NULL,
attempt_count integer NOT NULL DEFAULT 0,
next_attempt_at timestamptz NOT NULL DEFAULT now(),
lease_owner text,
lease_expires_at timestamptz,
delivered_at timestamptz,
last_error text NOT NULL DEFAULT '',
created_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL DEFAULT (now() + interval '24 hours'),
UNIQUE (join_event_id, template_id),
UNIQUE (target_user_id, ephemeral_id),
CONSTRAINT welcome_message_deliveries_shape CHECK (
join_event_id > 0 AND channel_id > 0 AND target_user_id > 0 AND
template_id > 0 AND ephemeral_id > 0 AND joined_at > 0 AND
jsonb_typeof(content) = 'object' AND pg_column_size(content) <= 4194304 AND
attempt_count >= 0 AND char_length(lease_owner) <= 128 AND
char_length(last_error) <= 1024 AND expires_at > created_at AND
expires_at <= created_at + interval '24 hours 1 second'
)
);
CREATE INDEX welcome_message_deliveries_due_idx
ON welcome_message_deliveries (next_attempt_at, id)
WHERE delivered_at IS NULL;
CREATE INDEX welcome_message_deliveries_expiry_idx
ON welcome_message_deliveries (expires_at, id);

View file

@ -0,0 +1 @@
DROP INDEX IF EXISTS welcome_message_deliveries_target_idx;

View file

@ -0,0 +1,2 @@
CREATE INDEX welcome_message_deliveries_target_idx
ON welcome_message_deliveries (channel_id, target_user_id, id);

View file

@ -0,0 +1,68 @@
DROP TRIGGER dispatch_outbox_insert_user_head ON dispatch_outbox;
DROP FUNCTION dispatch_outbox_insert_user_heads();
CREATE OR REPLACE FUNCTION dispatch_outbox_maintain_user_head()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
removed_head bigint;
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO dispatch_outbox_user_heads (
target_user_id, head_id, head_pts, status, next_attempt_at, updated_at
) VALUES (
NEW.target_user_id, NEW.id, NEW.pts, NEW.status, NEW.next_attempt_at, NEW.updated_at
)
ON CONFLICT (target_user_id) DO UPDATE
SET head_id = EXCLUDED.head_id,
head_pts = EXCLUDED.head_pts,
status = EXCLUDED.status,
next_attempt_at = EXCLUDED.next_attempt_at,
updated_at = EXCLUDED.updated_at
WHERE (EXCLUDED.head_pts, EXCLUDED.head_id) <
(dispatch_outbox_user_heads.head_pts, dispatch_outbox_user_heads.head_id);
RETURN NULL;
ELSIF TG_OP = 'UPDATE' THEN
UPDATE dispatch_outbox_user_heads
SET status = NEW.status,
next_attempt_at = NEW.next_attempt_at,
updated_at = NEW.updated_at
WHERE target_user_id = NEW.target_user_id
AND head_id = NEW.id;
RETURN NULL;
END IF;
DELETE FROM dispatch_outbox_user_heads
WHERE target_user_id = OLD.target_user_id
AND head_id = OLD.id
RETURNING head_id INTO removed_head;
IF removed_head IS NOT NULL THEN
INSERT INTO dispatch_outbox_user_heads (
target_user_id, head_id, head_pts, status, next_attempt_at, updated_at
)
SELECT target_user_id, id, pts, status, next_attempt_at, updated_at
FROM dispatch_outbox
WHERE target_user_id = OLD.target_user_id
ORDER BY pts ASC, id ASC
LIMIT 1
ON CONFLICT (target_user_id) DO UPDATE
SET head_id = EXCLUDED.head_id,
head_pts = EXCLUDED.head_pts,
status = EXCLUDED.status,
next_attempt_at = EXCLUDED.next_attempt_at,
updated_at = EXCLUDED.updated_at
WHERE (EXCLUDED.head_pts, EXCLUDED.head_id) <
(dispatch_outbox_user_heads.head_pts, dispatch_outbox_user_heads.head_id);
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER dispatch_outbox_insert_user_head
AFTER INSERT ON dispatch_outbox
FOR EACH ROW
EXECUTE FUNCTION dispatch_outbox_maintain_user_head();
DROP FUNCTION dispatch_outbox_lane_advisory_key(bigint);

View file

@ -0,0 +1,118 @@
-- Producers hold a shared per-lane fence until commit. A consumer that may
-- remove the last head takes the exclusive form in a preceding statement;
-- after that fence is acquired, its DELETE sees every earlier append or every
-- later append observes the missing marker and installs a new one.
CREATE FUNCTION dispatch_outbox_lane_advisory_key(target_user_id bigint)
RETURNS bigint
LANGUAGE sql
IMMUTABLE
STRICT
PARALLEL SAFE
AS $$
SELECT hashtextextended('telesrv:dispatch-outbox-lane:' || target_user_id::text, 0)
$$;
DROP TRIGGER dispatch_outbox_insert_user_head ON dispatch_outbox;
CREATE FUNCTION dispatch_outbox_insert_user_heads()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
PERFORM pg_advisory_xact_lock_shared(
dispatch_outbox_lane_advisory_key(streams.target_user_id)
)
FROM (
SELECT DISTINCT target_user_id
FROM new_rows
ORDER BY target_user_id
) AS streams;
-- The anti-join avoids touching a committed mutable head. ON CONFLICT is
-- retained only for concurrent producers racing to create an empty lane.
WITH candidates AS MATERIALIZED (
SELECT DISTINCT ON (target_user_id)
target_user_id, id AS head_id, pts AS head_pts,
status, next_attempt_at, updated_at
FROM new_rows
ORDER BY target_user_id, pts, id
)
INSERT INTO dispatch_outbox_user_heads (
target_user_id, head_id, head_pts, status, next_attempt_at, updated_at
)
SELECT
c.target_user_id, c.head_id, c.head_pts,
c.status, c.next_attempt_at, c.updated_at
FROM candidates c
WHERE NOT EXISTS (
SELECT 1
FROM dispatch_outbox_user_heads existing
WHERE existing.target_user_id = c.target_user_id
)
ON CONFLICT (target_user_id) DO NOTHING;
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION dispatch_outbox_maintain_user_head()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
removed_head bigint;
BEGIN
IF TG_OP = 'UPDATE' THEN
UPDATE dispatch_outbox_user_heads
SET status = NEW.status,
next_attempt_at = NEW.next_attempt_at,
updated_at = NEW.updated_at
WHERE target_user_id = NEW.target_user_id
AND head_id = NEW.id;
RETURN NULL;
END IF;
DELETE FROM dispatch_outbox_user_heads
WHERE target_user_id = OLD.target_user_id
AND head_id = OLD.id
RETURNING head_id INTO removed_head;
IF removed_head IS NOT NULL THEN
INSERT INTO dispatch_outbox_user_heads (
target_user_id, head_id, head_pts, status, next_attempt_at, updated_at
)
SELECT target_user_id, id, pts, status, next_attempt_at, updated_at
FROM dispatch_outbox
WHERE target_user_id = OLD.target_user_id
ORDER BY pts ASC, id ASC
LIMIT 1
ON CONFLICT (target_user_id) DO UPDATE
SET head_id = EXCLUDED.head_id,
head_pts = EXCLUDED.head_pts,
status = EXCLUDED.status,
next_attempt_at = EXCLUDED.next_attempt_at,
updated_at = EXCLUDED.updated_at
WHERE (EXCLUDED.head_pts, EXCLUDED.head_id) <
(dispatch_outbox_user_heads.head_pts, dispatch_outbox_user_heads.head_id);
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER dispatch_outbox_insert_user_head
AFTER INSERT ON dispatch_outbox
REFERENCING NEW TABLE AS new_rows
FOR EACH STATEMENT
EXECUTE FUNCTION dispatch_outbox_insert_user_heads();
-- Repair any marker that may be absent when upgrading from an interrupted or
-- older deployment. Startup migration runs before the dispatcher starts.
INSERT INTO dispatch_outbox_user_heads (
target_user_id, head_id, head_pts, status, next_attempt_at, updated_at
)
SELECT DISTINCT ON (d.target_user_id)
d.target_user_id, d.id, d.pts, d.status, d.next_attempt_at, d.updated_at
FROM dispatch_outbox d
LEFT JOIN dispatch_outbox_user_heads h USING (target_user_id)
WHERE h.target_user_id IS NULL
ORDER BY d.target_user_id, d.pts, d.id
ON CONFLICT (target_user_id) DO NOTHING;

View file

@ -0,0 +1,11 @@
DROP TRIGGER IF EXISTS channel_message_reactions_dialog_top_projection_changed ON channel_message_reactions;
DROP FUNCTION IF EXISTS telesrv_notify_channel_top_reactions_read_model();
DROP TRIGGER IF EXISTS channel_messages_dialog_top_projection_changed ON channel_messages;
DROP FUNCTION IF EXISTS telesrv_notify_channel_top_message_read_model();
DROP TRIGGER IF EXISTS private_message_reactions_dialog_top_projection_changed ON private_message_reactions;
DROP FUNCTION IF EXISTS telesrv_notify_private_top_reactions_read_model();
DROP TRIGGER IF EXISTS message_boxes_dialog_top_projection_changed ON message_boxes;
DROP FUNCTION IF EXISTS telesrv_notify_private_top_message_read_model();

View file

@ -0,0 +1,135 @@
-- Complete the dialog_light/channel_base dependency closure for cached dialog
-- top-message projections. These triggers only bump read-model versions; they
-- do not allocate PTS or mutate message/update facts.
CREATE OR REPLACE FUNCTION telesrv_notify_private_top_message_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF EXISTS (
SELECT 1
FROM dialogs d
WHERE d.user_id = NEW.owner_user_id
AND d.peer_type = NEW.peer_type
AND d.peer_id = NEW.peer_id
AND d.top_message_id = NEW.box_id
) THEN
PERFORM telesrv_bump_dialog_light(NEW.owner_user_id, NEW.peer_type, NEW.peer_id);
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER message_boxes_dialog_top_projection_changed
AFTER UPDATE OF
message_date, body, entities, deleted, edit_date, silent, noforwards,
reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id,
reply_to_story_id, quote_text, quote_entities, quote_offset,
fwd_from_peer_type, fwd_from_peer_id, fwd_from_name, fwd_date,
fwd_saved_from_peer_type, fwd_saved_from_peer_id, fwd_saved_from_msg_id,
media, media_unread, reaction_unread, ttl_period, expires_at, pinned,
saved_peer_type, saved_peer_id, reply_markup, via_bot_id, rich_message,
grouped_id, effect, hide_edited
ON message_boxes
FOR EACH ROW
EXECUTE FUNCTION telesrv_notify_private_top_message_read_model();
CREATE OR REPLACE FUNCTION telesrv_notify_private_top_reactions_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
changed_sender_id BIGINT;
changed_message_id BIGINT;
BEGIN
IF TG_OP = 'DELETE' THEN
changed_sender_id := OLD.message_sender_id;
changed_message_id := OLD.private_message_id;
ELSE
changed_sender_id := NEW.message_sender_id;
changed_message_id := NEW.private_message_id;
END IF;
PERFORM telesrv_bump_dialog_light(b.owner_user_id, b.peer_type, b.peer_id)
FROM message_boxes b
JOIN dialogs d
ON d.user_id = b.owner_user_id
AND d.peer_type = b.peer_type
AND d.peer_id = b.peer_id
AND d.top_message_id = b.box_id
WHERE b.message_sender_id = changed_sender_id
AND b.private_message_id = changed_message_id
AND NOT b.deleted;
RETURN NULL;
END;
$$;
CREATE TRIGGER private_message_reactions_dialog_top_projection_changed
AFTER INSERT OR DELETE OR UPDATE
ON private_message_reactions
FOR EACH ROW
EXECUTE FUNCTION telesrv_notify_private_top_reactions_read_model();
CREATE OR REPLACE FUNCTION telesrv_notify_channel_top_message_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF EXISTS (
SELECT 1
FROM channels c
WHERE c.id = NEW.channel_id
AND c.top_message_id = NEW.id
) THEN
PERFORM telesrv_bump_read_model_version('channel_base', 0, 'channel', NEW.channel_id);
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER channel_messages_dialog_top_projection_changed
AFTER UPDATE OF
sender_user_id, from_peer_type, from_peer_id, send_as_peer_type, send_as_peer_id,
message_date, edit_date, post, silent, noforwards, body, entities, reply_to,
reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id, fwd_from,
discussion_channel_id, discussion_message_id, action, deleted, views_count, media,
ttl_period, expires_at, post_author, pinned, via_bot_id, reply_markup,
from_boosts_applied, rich_message
ON channel_messages
FOR EACH ROW
EXECUTE FUNCTION telesrv_notify_channel_top_message_read_model();
CREATE OR REPLACE FUNCTION telesrv_notify_channel_top_reactions_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
changed_channel_id BIGINT;
changed_message_id INTEGER;
BEGIN
IF TG_OP = 'DELETE' THEN
changed_channel_id := OLD.channel_id;
changed_message_id := OLD.message_id;
ELSE
changed_channel_id := NEW.channel_id;
changed_message_id := NEW.message_id;
END IF;
IF EXISTS (
SELECT 1
FROM channels c
WHERE c.id = changed_channel_id
AND c.top_message_id = changed_message_id
) THEN
PERFORM telesrv_bump_read_model_version('channel_base', 0, 'channel', changed_channel_id);
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER channel_message_reactions_dialog_top_projection_changed
AFTER INSERT OR DELETE OR UPDATE
ON channel_message_reactions
FOR EACH ROW
EXECUTE FUNCTION telesrv_notify_channel_top_reactions_read_model();

View file

@ -0,0 +1,7 @@
DROP TRIGGER IF EXISTS communities_bump_catalog_read_model ON public.communities;
DROP FUNCTION IF EXISTS public.telesrv_bump_community_catalog_read_model();
DELETE FROM public.read_model_versions
WHERE model = 'community_catalog'
AND owner_user_id = 0
AND peer_type = 'community'
AND peer_id = 0;

View file

@ -0,0 +1,34 @@
-- Global Community-catalog presence token. messages.getDialogs needs collapsed
-- Communities only when at least one live Community exists; without this
-- durable invalidation token an empty deployment would repeat the expensive
-- owner-membership query on every dialogs page.
CREATE OR REPLACE FUNCTION public.telesrv_bump_community_catalog_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
PERFORM public.telesrv_bump_read_model_version(
'community_catalog',
0,
'community',
0
);
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS communities_bump_catalog_read_model ON public.communities;
CREATE TRIGGER communities_bump_catalog_read_model
AFTER INSERT OR UPDATE OR DELETE ON public.communities
FOR EACH ROW EXECUTE FUNCTION public.telesrv_bump_community_catalog_read_model();
SELECT public.telesrv_bump_read_model_version(
'community_catalog',
0,
'community',
0
);

View file

@ -0,0 +1,7 @@
DROP FUNCTION IF EXISTS public.telesrv_advance_auth_session_layer(
bigint,
bigint,
integer,
bigint,
timestamptz
);

View file

@ -0,0 +1,245 @@
-- Advance one explicit invokeWithLayer observation without paying one client
-- round trip for every lock, comparison and projection update. The caller
-- still owns the surrounding transaction/savepoint so identity_changed can
-- roll the complete attempt back and retry with a fresh READ COMMITTED view.
CREATE OR REPLACE FUNCTION public.telesrv_advance_auth_session_layer(
p_raw_auth_key_id bigint,
p_session_id bigint,
p_layer integer,
p_msg_id bigint,
p_expires_at timestamptz
)
RETURNS TABLE (
advance_status text,
current_layer integer,
current_msg_id bigint,
current_observation_id bigint,
current_expires_at timestamptz,
shared_default boolean,
applied boolean
)
LANGUAGE plpgsql
AS $$
DECLARE
v_now timestamptz := now();
v_hint_expiry integer;
v_hint_perm_id bigint;
v_hint_bound boolean := false;
v_hint_has_identity boolean := false;
v_hint_identity_id bigint;
v_raw_expiry integer;
v_actual_perm_id bigint;
v_actual_bound boolean := false;
v_actual_has_identity boolean := false;
v_actual_identity_id bigint;
v_perm_expiry integer;
v_current_found boolean := false;
v_current_layer integer := 0;
v_current_msg_id bigint := 0;
v_current_observation_id bigint := 0;
v_current_expires_at timestamptz := p_expires_at;
v_shared_default boolean := false;
v_observation_id bigint;
v_key_ids bigint[];
v_updated integer;
BEGIN
IF p_layer <= 0
OR p_msg_id <= 0
OR p_msg_id % 4 <> 0
OR (p_msg_id & 4294967295) = 0
OR p_expires_at IS NULL
OR NOT v_now < p_expires_at
OR p_expires_at - interval '301 seconds' > v_now + interval '30 seconds'
THEN
RETURN QUERY SELECT
'evidence_invalid'::text, 0, 0::bigint, 0::bigint,
COALESCE(p_expires_at, v_now), false, false;
RETURN;
END IF;
-- Lock-free identity hint. It is deliberately revalidated after the raw
-- row lock; a newly committed first bind returns identity_changed rather
-- than taking a permanent identity lock in raw->identity order.
SELECT key.expires_at, binding.perm_auth_key_id
INTO v_hint_expiry, v_hint_perm_id
FROM public.auth_keys AS key
LEFT JOIN public.temp_auth_key_bindings AS binding
ON binding.temp_auth_key_id = key.auth_key_id
WHERE key.auth_key_id = p_raw_auth_key_id;
IF NOT FOUND THEN
RETURN QUERY SELECT
'auth_key_not_found'::text, 0, 0::bigint, 0::bigint,
p_expires_at, false, false;
RETURN;
END IF;
v_hint_bound := v_hint_perm_id IS NOT NULL;
v_hint_has_identity := v_hint_bound OR v_hint_expiry = 0;
IF v_hint_has_identity THEN
v_hint_identity_id := CASE
WHEN v_hint_bound THEN v_hint_perm_id
ELSE p_raw_auth_key_id
END;
PERFORM pg_advisory_xact_lock(
1096111176::integer,
hashint8(v_hint_identity_id)::integer
);
END IF;
SELECT key.expires_at
INTO v_raw_expiry
FROM public.auth_keys AS key
WHERE key.auth_key_id = p_raw_auth_key_id
FOR UPDATE;
IF NOT FOUND THEN
RETURN QUERY SELECT
'auth_key_not_found'::text, 0, 0::bigint, 0::bigint,
p_expires_at, false, false;
RETURN;
END IF;
v_actual_perm_id := NULL;
SELECT binding.perm_auth_key_id
INTO v_actual_perm_id
FROM public.temp_auth_key_bindings AS binding
WHERE binding.temp_auth_key_id = p_raw_auth_key_id;
v_actual_bound := FOUND;
IF NOT v_actual_bound THEN
v_actual_perm_id := p_raw_auth_key_id;
END IF;
v_actual_has_identity := v_actual_bound OR v_raw_expiry = 0;
v_actual_identity_id := v_actual_perm_id;
IF v_actual_has_identity <> v_hint_has_identity
OR (v_actual_has_identity AND v_actual_identity_id <> v_hint_identity_id)
OR v_actual_bound <> v_hint_bound
OR v_raw_expiry <> v_hint_expiry
THEN
RETURN QUERY SELECT
'identity_changed'::text, 0, 0::bigint, 0::bigint,
p_expires_at, false, false;
RETURN;
END IF;
IF v_actual_bound THEN
SELECT key.expires_at
INTO v_perm_expiry
FROM public.auth_keys AS key
WHERE key.auth_key_id = v_actual_perm_id
FOR UPDATE;
IF NOT FOUND OR v_raw_expiry <= 0 OR v_perm_expiry <> 0 THEN
RETURN QUERY SELECT
'binding_invalid'::text, 0, 0::bigint, 0::bigint,
p_expires_at, false, false;
RETURN;
END IF;
END IF;
SELECT evidence.layer,
evidence.msg_id,
evidence.observation_id,
evidence.expires_at
INTO v_current_layer,
v_current_msg_id,
v_current_observation_id,
v_current_expires_at
FROM public.auth_key_session_layers AS evidence
WHERE evidence.raw_auth_key_id = p_raw_auth_key_id
AND evidence.session_id = p_session_id
FOR UPDATE;
v_current_found := FOUND;
IF v_current_found AND v_now < v_current_expires_at THEN
IF p_msg_id < v_current_msg_id THEN
SELECT key.layer = v_current_layer
AND key.layer_observation_id = v_current_observation_id
INTO v_shared_default
FROM public.auth_keys AS key
WHERE key.auth_key_id = v_actual_perm_id;
RETURN QUERY SELECT
'ok'::text, v_current_layer, v_current_msg_id,
v_current_observation_id, v_current_expires_at,
v_shared_default, false;
RETURN;
END IF;
IF p_msg_id = v_current_msg_id THEN
IF p_layer <> v_current_layer THEN
RETURN QUERY SELECT
'conflict'::text, v_current_layer, v_current_msg_id,
v_current_observation_id, v_current_expires_at,
false, false;
RETURN;
END IF;
SELECT key.layer = v_current_layer
AND key.layer_observation_id = v_current_observation_id
INTO v_shared_default
FROM public.auth_keys AS key
WHERE key.auth_key_id = v_actual_perm_id;
RETURN QUERY SELECT
'ok'::text, v_current_layer, v_current_msg_id,
v_current_observation_id, v_current_expires_at,
v_shared_default, false;
RETURN;
END IF;
END IF;
v_observation_id := nextval('public.auth_key_layer_observation_seq');
INSERT INTO public.auth_key_session_layers (
raw_auth_key_id,
session_id,
layer,
msg_id,
observation_id,
expires_at
) VALUES (
p_raw_auth_key_id,
p_session_id,
p_layer,
p_msg_id,
v_observation_id,
p_expires_at
)
ON CONFLICT (raw_auth_key_id, session_id) DO UPDATE SET
layer = EXCLUDED.layer,
msg_id = EXCLUDED.msg_id,
observation_id = EXCLUDED.observation_id,
expires_at = EXCLUDED.expires_at
RETURNING layer, msg_id, observation_id, expires_at
INTO v_current_layer,
v_current_msg_id,
v_current_observation_id,
v_current_expires_at;
v_key_ids := ARRAY[p_raw_auth_key_id];
IF v_actual_perm_id <> p_raw_auth_key_id THEN
v_key_ids := array_append(v_key_ids, v_actual_perm_id);
END IF;
UPDATE public.auth_keys AS key
SET layer = p_layer,
layer_observation_id = v_observation_id
WHERE key.auth_key_id = ANY(v_key_ids)
AND key.layer_observation_id < v_observation_id;
GET DIAGNOSTICS v_updated = ROW_COUNT;
IF v_updated <> cardinality(v_key_ids) THEN
RAISE EXCEPTION
'publish auth session Layer defaults updated % of % locked keys',
v_updated,
cardinality(v_key_ids)
USING ERRCODE = '23000';
END IF;
UPDATE public.authorizations AS authz
SET layer = p_layer
WHERE authz.auth_key_id = ANY(v_key_ids);
RETURN QUERY SELECT
'ok'::text, v_current_layer, v_current_msg_id,
v_current_observation_id, v_current_expires_at,
true, true;
END;
$$;
COMMENT ON FUNCTION public.telesrv_advance_auth_session_layer(
bigint, bigint, integer, bigint, timestamptz
) IS 'Atomically advances durable invokeWithLayer evidence while preserving auth identity lock order';

View file

@ -0,0 +1,7 @@
DROP TRIGGER IF EXISTS custom_verifications_peer_identity_changed ON public.custom_verifications;
DROP TRIGGER IF EXISTS peer_usernames_peer_identity_changed ON public.peer_usernames;
DROP FUNCTION IF EXISTS public.telesrv_notify_peer_identity_row();
DROP FUNCTION IF EXISTS public.telesrv_bump_peer_identity(text, bigint);
DELETE FROM public.read_model_versions
WHERE model = 'peer_identity';

View file

@ -0,0 +1,68 @@
-- Viewer-independent peer decorations are projected on every peer-bearing RPC
-- response. Keep one durable version token for username registry vectors and
-- third-party bot-verification marks so positive and negative L1 entries can be
-- reused without one PostgreSQL query per response page.
CREATE OR REPLACE FUNCTION public.telesrv_bump_peer_identity(
p_peer_type text,
p_peer_id bigint
) RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
IF COALESCE(p_peer_id, 0) = 0 OR p_peer_type NOT IN ('user', 'channel') THEN
RETURN;
END IF;
PERFORM public.telesrv_bump_read_model_version(
'peer_identity', 0, p_peer_type, p_peer_id
);
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_peer_identity_row()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
PERFORM public.telesrv_bump_peer_identity(OLD.peer_type, OLD.peer_id);
RETURN OLD;
END IF;
IF TG_OP = 'UPDATE'
AND (OLD.peer_type, OLD.peer_id) IS DISTINCT FROM (NEW.peer_type, NEW.peer_id) THEN
PERFORM public.telesrv_bump_peer_identity(OLD.peer_type, OLD.peer_id);
END IF;
PERFORM public.telesrv_bump_peer_identity(NEW.peer_type, NEW.peer_id);
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS peer_usernames_peer_identity_changed ON public.peer_usernames;
CREATE TRIGGER peer_usernames_peer_identity_changed
AFTER INSERT OR UPDATE OR DELETE ON public.peer_usernames
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_peer_identity_row();
DROP TRIGGER IF EXISTS custom_verifications_peer_identity_changed ON public.custom_verifications;
CREATE TRIGGER custom_verifications_peer_identity_changed
AFTER INSERT OR UPDATE OR DELETE ON public.custom_verifications
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_peer_identity_row();
-- Seed every durable peer, including peers with no registry row/mark. This is
-- what makes an empty result a versioned negative fact rather than a TTL guess.
INSERT INTO public.read_model_versions (
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
)
SELECT 'peer_identity', 0, 'user', u.id, 1,
public.telesrv_random_read_model_hash(), now()
FROM public.users u
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO NOTHING;
INSERT INTO public.read_model_versions (
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
)
SELECT 'peer_identity', 0, 'channel', c.id, 1,
public.telesrv_random_read_model_hash(), now()
FROM public.channels c
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO NOTHING;

View file

@ -0,0 +1,43 @@
DROP TRIGGER IF EXISTS users_story_projection_versions_changed ON public.users;
DROP TRIGGER IF EXISTS channels_story_projection_version_changed ON public.channels;
DROP TRIGGER IF EXISTS telesrv_stories_story_peer_read_model ON public.stories;
DROP TRIGGER IF EXISTS telesrv_story_hidden_peers_story_peer_read_model ON public.story_hidden_peers;
DROP FUNCTION IF EXISTS public.telesrv_maintain_user_story_projection_versions();
DROP FUNCTION IF EXISTS public.telesrv_maintain_channel_story_projection_version();
DROP FUNCTION IF EXISTS public.telesrv_notify_story_row_read_models();
DROP FUNCTION IF EXISTS public.telesrv_notify_story_hidden_row_read_models();
DROP FUNCTION IF EXISTS public.telesrv_bump_story_hidden_list(bigint);
DROP FUNCTION IF EXISTS public.telesrv_bump_story_peer(text, bigint);
CREATE OR REPLACE FUNCTION public.telesrv_notify_story_peer_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
o_type text;
o_id bigint;
BEGIN
IF TG_OP = 'DELETE' THEN
o_type := OLD.owner_peer_type;
o_id := OLD.owner_peer_id;
ELSE
o_type := NEW.owner_peer_type;
o_id := NEW.owner_peer_id;
END IF;
PERFORM public.telesrv_bump_read_model_version('story_peer', 0, o_type, o_id);
RETURN NULL;
END;
$$;
CREATE TRIGGER telesrv_stories_story_peer_read_model
AFTER INSERT OR UPDATE OR DELETE ON public.stories
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_story_peer_read_model();
CREATE TRIGGER telesrv_story_hidden_peers_story_peer_read_model
AFTER INSERT OR UPDATE OR DELETE ON public.story_hidden_peers
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_story_peer_read_model();
DELETE FROM public.read_model_versions WHERE model = 'story_hidden_list';
-- story_peer rows seeded for peers without stories are harmless under the
-- previous contract and are retained so rollback cannot erase real versions.

View file

@ -0,0 +1,161 @@
-- Split story peer projection into a shared active-story gate and one sparse
-- hidden-peer snapshot per viewer. Both positive and negative facts receive a
-- durable token; active expiry is still enforced by expire_date at read time.
CREATE OR REPLACE FUNCTION public.telesrv_bump_story_peer(
p_peer_type text,
p_peer_id bigint
) RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
IF COALESCE(p_peer_type, '') NOT IN ('user', 'channel')
OR COALESCE(p_peer_id, 0) = 0 THEN
RETURN;
END IF;
PERFORM public.telesrv_bump_read_model_version(
'story_peer', 0, p_peer_type, p_peer_id
);
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_bump_story_hidden_list(
p_viewer_user_id bigint
) RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
IF COALESCE(p_viewer_user_id, 0) = 0 THEN
RETURN;
END IF;
PERFORM public.telesrv_bump_read_model_version(
'story_hidden_list', p_viewer_user_id, 'user', p_viewer_user_id
);
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_story_row_read_models()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
PERFORM public.telesrv_bump_story_peer(OLD.owner_peer_type, OLD.owner_peer_id);
RETURN OLD;
END IF;
IF TG_OP = 'UPDATE'
AND (OLD.owner_peer_type, OLD.owner_peer_id)
IS DISTINCT FROM (NEW.owner_peer_type, NEW.owner_peer_id) THEN
PERFORM public.telesrv_bump_story_peer(OLD.owner_peer_type, OLD.owner_peer_id);
END IF;
PERFORM public.telesrv_bump_story_peer(NEW.owner_peer_type, NEW.owner_peer_id);
RETURN NEW;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_story_hidden_row_read_models()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
PERFORM public.telesrv_bump_story_peer(OLD.owner_peer_type, OLD.owner_peer_id);
PERFORM public.telesrv_bump_story_hidden_list(OLD.viewer_user_id);
RETURN OLD;
END IF;
IF TG_OP = 'UPDATE' THEN
IF (OLD.owner_peer_type, OLD.owner_peer_id)
IS DISTINCT FROM (NEW.owner_peer_type, NEW.owner_peer_id) THEN
PERFORM public.telesrv_bump_story_peer(OLD.owner_peer_type, OLD.owner_peer_id);
END IF;
IF OLD.viewer_user_id IS DISTINCT FROM NEW.viewer_user_id THEN
PERFORM public.telesrv_bump_story_hidden_list(OLD.viewer_user_id);
END IF;
END IF;
PERFORM public.telesrv_bump_story_peer(NEW.owner_peer_type, NEW.owner_peer_id);
PERFORM public.telesrv_bump_story_hidden_list(NEW.viewer_user_id);
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS telesrv_stories_story_peer_read_model ON public.stories;
CREATE TRIGGER telesrv_stories_story_peer_read_model
AFTER INSERT OR UPDATE OR DELETE ON public.stories
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_story_row_read_models();
DROP TRIGGER IF EXISTS telesrv_story_hidden_peers_story_peer_read_model ON public.story_hidden_peers;
CREATE TRIGGER telesrv_story_hidden_peers_story_peer_read_model
AFTER INSERT OR UPDATE OR DELETE ON public.story_hidden_peers
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_story_hidden_row_read_models();
CREATE OR REPLACE FUNCTION public.telesrv_maintain_user_story_projection_versions()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
PERFORM public.telesrv_bump_story_peer('user', OLD.id);
PERFORM public.telesrv_bump_story_hidden_list(OLD.id);
DELETE FROM public.read_model_versions
WHERE (model = 'story_peer' AND owner_user_id = 0
AND peer_type = 'user' AND peer_id = OLD.id)
OR (model = 'story_hidden_list' AND owner_user_id = OLD.id
AND peer_type = 'user' AND peer_id = OLD.id);
RETURN OLD;
END IF;
PERFORM public.telesrv_bump_story_peer('user', NEW.id);
PERFORM public.telesrv_bump_story_hidden_list(NEW.id);
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS users_story_projection_versions_changed ON public.users;
CREATE TRIGGER users_story_projection_versions_changed
AFTER INSERT OR DELETE ON public.users
FOR EACH ROW EXECUTE FUNCTION public.telesrv_maintain_user_story_projection_versions();
CREATE OR REPLACE FUNCTION public.telesrv_maintain_channel_story_projection_version()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
PERFORM public.telesrv_bump_story_peer('channel', OLD.id);
DELETE FROM public.read_model_versions
WHERE model = 'story_peer' AND owner_user_id = 0
AND peer_type = 'channel' AND peer_id = OLD.id;
RETURN OLD;
END IF;
PERFORM public.telesrv_bump_story_peer('channel', NEW.id);
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS channels_story_projection_version_changed ON public.channels;
CREATE TRIGGER channels_story_projection_version_changed
AFTER INSERT OR DELETE ON public.channels
FOR EACH ROW EXECUTE FUNCTION public.telesrv_maintain_channel_story_projection_version();
INSERT INTO public.read_model_versions (
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
)
SELECT 'story_peer', 0, 'user', u.id, 1,
public.telesrv_random_read_model_hash(), now()
FROM public.users u
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO NOTHING;
INSERT INTO public.read_model_versions (
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
)
SELECT 'story_peer', 0, 'channel', c.id, 1,
public.telesrv_random_read_model_hash(), now()
FROM public.channels c
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO NOTHING;
INSERT INTO public.read_model_versions (
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
)
SELECT 'story_hidden_list', u.id, 'user', u.id, 1,
public.telesrv_random_read_model_hash(), now()
FROM public.users u
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO NOTHING;

View file

@ -0,0 +1,95 @@
DROP TRIGGER IF EXISTS users_dialog_owner_read_model ON public.users;
DROP FUNCTION IF EXISTS public.telesrv_seed_dialog_owner_read_model();
DELETE FROM public.read_model_versions WHERE model = 'dialog_owner';
CREATE OR REPLACE FUNCTION public.telesrv_notify_dialog_light_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
owner_id bigint;
peer_type text;
peer_id bigint;
BEGIN
IF TG_OP = 'DELETE' THEN
owner_id := OLD.user_id;
peer_type := OLD.peer_type;
peer_id := OLD.peer_id;
ELSE
owner_id := NEW.user_id;
peer_type := NEW.peer_type;
peer_id := NEW.peer_id;
END IF;
PERFORM public.telesrv_bump_dialog_light(owner_id, peer_type, peer_id);
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_dialog_light_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
owner_id bigint;
channel_id bigint;
BEGIN
IF TG_OP = 'DELETE' THEN
owner_id := OLD.user_id;
channel_id := OLD.channel_id;
ELSE
owner_id := NEW.user_id;
channel_id := NEW.channel_id;
END IF;
PERFORM public.telesrv_bump_dialog_light(owner_id, 'channel', channel_id);
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_member_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
channel_id bigint;
user_id bigint;
BEGIN
IF TG_OP = 'DELETE' THEN
channel_id := OLD.channel_id;
user_id := OLD.user_id;
ELSE
channel_id := NEW.channel_id;
user_id := NEW.user_id;
END IF;
PERFORM public.telesrv_bump_read_model_version('channel_member', user_id, 'channel', channel_id);
PERFORM public.telesrv_bump_dialog_light(user_id, 'channel', channel_id);
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_bump_dialog_light(
p_owner_user_id bigint,
p_peer_type text,
p_peer_id bigint
)
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
IF COALESCE(p_owner_user_id, 0) = 0
OR COALESCE(p_peer_id, 0) = 0
OR COALESCE(p_peer_type, '') = ''
THEN
RETURN;
END IF;
PERFORM public.telesrv_bump_read_model_version(
'dialog_light', p_owner_user_id, p_peer_type, p_peer_id
);
END;
$$;
DROP FUNCTION IF EXISTS public.telesrv_bump_dialog_owner(bigint);

View file

@ -0,0 +1,161 @@
-- Owner collection generation for the Redis-backed messages.getDialogs header
-- snapshot. Shared channel mutations remain channel_base-only; membership and
-- owner-local dialog mutations advance this O(1) owner token.
CREATE OR REPLACE FUNCTION public.telesrv_bump_dialog_owner(p_owner_user_id bigint)
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
IF COALESCE(p_owner_user_id, 0) = 0 THEN
RETURN;
END IF;
PERFORM public.telesrv_bump_read_model_version(
'dialog_owner', p_owner_user_id, 'user', p_owner_user_id
);
END;
$$;
-- dialog_owner is the owner-wide aggregate of every exact dialog_light
-- dependency. Extending the common helper closes private top-message,
-- reaction, contact/profile and future exact-dialog invalidation paths without
-- copying owner bumps into every trigger.
CREATE OR REPLACE FUNCTION public.telesrv_bump_dialog_light(
p_owner_user_id bigint,
p_peer_type text,
p_peer_id bigint
)
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
IF COALESCE(p_owner_user_id, 0) = 0
OR COALESCE(p_peer_id, 0) = 0
OR COALESCE(p_peer_type, '') = ''
THEN
RETURN;
END IF;
PERFORM public.telesrv_bump_read_model_version(
'dialog_light', p_owner_user_id, p_peer_type, p_peer_id
);
PERFORM public.telesrv_bump_dialog_owner(p_owner_user_id);
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_seed_dialog_owner_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
PERFORM public.telesrv_bump_dialog_owner(NEW.id);
RETURN NULL;
END;
$$;
CREATE TRIGGER users_dialog_owner_read_model
AFTER INSERT ON public.users
FOR EACH ROW EXECUTE FUNCTION public.telesrv_seed_dialog_owner_read_model();
CREATE OR REPLACE FUNCTION public.telesrv_notify_dialog_light_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
old_owner_id bigint;
old_peer_type text;
old_peer_id bigint;
new_owner_id bigint;
new_peer_type text;
new_peer_id bigint;
BEGIN
IF TG_OP <> 'INSERT' THEN
old_owner_id := OLD.user_id;
old_peer_type := OLD.peer_type;
old_peer_id := OLD.peer_id;
PERFORM public.telesrv_bump_dialog_light(old_owner_id, old_peer_type, old_peer_id);
END IF;
IF TG_OP <> 'DELETE' THEN
new_owner_id := NEW.user_id;
new_peer_type := NEW.peer_type;
new_peer_id := NEW.peer_id;
IF TG_OP = 'INSERT'
OR (new_owner_id, new_peer_type, new_peer_id)
IS DISTINCT FROM (old_owner_id, old_peer_type, old_peer_id)
THEN
PERFORM public.telesrv_bump_dialog_light(new_owner_id, new_peer_type, new_peer_id);
END IF;
END IF;
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_dialog_light_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
old_owner_id bigint;
old_channel_id bigint;
new_owner_id bigint;
new_channel_id bigint;
BEGIN
IF TG_OP <> 'INSERT' THEN
old_owner_id := OLD.user_id;
old_channel_id := OLD.channel_id;
PERFORM public.telesrv_bump_dialog_light(old_owner_id, 'channel', old_channel_id);
END IF;
IF TG_OP <> 'DELETE' THEN
new_owner_id := NEW.user_id;
new_channel_id := NEW.channel_id;
IF TG_OP = 'INSERT'
OR (new_owner_id, new_channel_id) IS DISTINCT FROM (old_owner_id, old_channel_id)
THEN
PERFORM public.telesrv_bump_dialog_light(new_owner_id, 'channel', new_channel_id);
END IF;
END IF;
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_member_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
old_channel_id bigint;
old_user_id bigint;
new_channel_id bigint;
new_user_id bigint;
BEGIN
IF TG_OP <> 'INSERT' THEN
old_channel_id := OLD.channel_id;
old_user_id := OLD.user_id;
PERFORM public.telesrv_bump_read_model_version('channel_member', old_user_id, 'channel', old_channel_id);
PERFORM public.telesrv_bump_dialog_light(old_user_id, 'channel', old_channel_id);
END IF;
IF TG_OP <> 'DELETE' THEN
new_channel_id := NEW.channel_id;
new_user_id := NEW.user_id;
IF TG_OP = 'INSERT'
OR (new_user_id, new_channel_id) IS DISTINCT FROM (old_user_id, old_channel_id)
THEN
PERFORM public.telesrv_bump_read_model_version('channel_member', new_user_id, 'channel', new_channel_id);
PERFORM public.telesrv_bump_dialog_light(new_user_id, 'channel', new_channel_id);
END IF;
END IF;
RETURN NULL;
END;
$$;
INSERT INTO public.read_model_versions (
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
)
SELECT 'dialog_owner', u.id, 'user', u.id, 1,
public.telesrv_random_read_model_hash(), now()
FROM public.users AS u
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO NOTHING;

View file

@ -0,0 +1,109 @@
DROP FUNCTION IF EXISTS public.telesrv_bump_channel_membership_read_models(bigint, bigint[]);
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_member_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
old_channel_id bigint;
old_user_id bigint;
new_channel_id bigint;
new_user_id bigint;
BEGIN
IF TG_OP <> 'INSERT' THEN
old_channel_id := OLD.channel_id;
old_user_id := OLD.user_id;
PERFORM public.telesrv_bump_read_model_version('channel_member', old_user_id, 'channel', old_channel_id);
PERFORM public.telesrv_bump_dialog_light(old_user_id, 'channel', old_channel_id);
END IF;
IF TG_OP <> 'DELETE' THEN
new_channel_id := NEW.channel_id;
new_user_id := NEW.user_id;
IF TG_OP = 'INSERT' OR (new_user_id, new_channel_id) IS DISTINCT FROM (old_user_id, old_channel_id) THEN
PERFORM public.telesrv_bump_read_model_version('channel_member', new_user_id, 'channel', new_channel_id);
PERFORM public.telesrv_bump_dialog_light(new_user_id, 'channel', new_channel_id);
END IF;
END IF;
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_participants_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
old_channel_id bigint;
new_channel_id bigint;
BEGIN
IF TG_OP = 'DELETE' THEN old_channel_id := OLD.channel_id;
ELSIF TG_OP = 'INSERT' THEN new_channel_id := NEW.channel_id;
ELSE old_channel_id := OLD.channel_id; new_channel_id := NEW.channel_id;
END IF;
IF old_channel_id IS NOT NULL THEN
PERFORM public.telesrv_bump_read_model_version('channel_participants', 0, 'channel', old_channel_id);
END IF;
IF new_channel_id IS NOT NULL AND new_channel_id IS DISTINCT FROM old_channel_id THEN
PERFORM public.telesrv_bump_read_model_version('channel_participants', 0, 'channel', new_channel_id);
END IF;
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_dialog_light_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
old_owner_id bigint;
old_channel_id bigint;
new_owner_id bigint;
new_channel_id bigint;
BEGIN
IF TG_OP <> 'INSERT' THEN
old_owner_id := OLD.user_id; old_channel_id := OLD.channel_id;
PERFORM public.telesrv_bump_dialog_light(old_owner_id, 'channel', old_channel_id);
END IF;
IF TG_OP <> 'DELETE' THEN
new_owner_id := NEW.user_id; new_channel_id := NEW.channel_id;
IF TG_OP = 'INSERT' OR (new_owner_id, new_channel_id) IS DISTINCT FROM (old_owner_id, old_channel_id) THEN
PERFORM public.telesrv_bump_dialog_light(new_owner_id, 'channel', new_channel_id);
END IF;
END IF;
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_active_memberships_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
old_owner bigint;
new_owner bigint;
changed boolean;
BEGIN
IF TG_OP = 'DELETE' THEN
old_owner := OLD.user_id;
IF old_owner <> 0 THEN PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', old_owner, 'user', old_owner); END IF;
RETURN NULL;
END IF;
new_owner := NEW.user_id;
IF TG_OP = 'INSERT' THEN
IF new_owner <> 0 THEN PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', new_owner, 'user', new_owner); END IF;
RETURN NULL;
END IF;
old_owner := OLD.user_id;
changed := OLD.user_id IS DISTINCT FROM NEW.user_id OR OLD.channel_id IS DISTINCT FROM NEW.channel_id OR
OLD.status IS DISTINCT FROM NEW.status OR OLD.deleted IS DISTINCT FROM NEW.deleted;
IF changed THEN
IF old_owner <> 0 THEN PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', old_owner, 'user', old_owner); END IF;
IF new_owner <> 0 AND new_owner IS DISTINCT FROM old_owner THEN
PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', new_owner, 'user', new_owner);
END IF;
END IF;
RETURN NULL;
END;
$$;
DROP FUNCTION IF EXISTS public.telesrv_membership_batch_active();

View file

@ -0,0 +1,221 @@
-- Batch membership mutations are one durable state transition. Row triggers
-- remain authoritative for ordinary statements, while the explicitly scoped
-- batch path suppresses their per-row write amplification and advances every
-- distinct dependency once before the transaction may commit.
CREATE OR REPLACE FUNCTION public.telesrv_membership_batch_active()
RETURNS boolean
LANGUAGE sql
STABLE
AS $$
SELECT COALESCE(current_setting('telesrv.membership_batch_mode', true), '') = 'on'
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_member_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
old_channel_id bigint;
old_user_id bigint;
new_channel_id bigint;
new_user_id bigint;
BEGIN
IF public.telesrv_membership_batch_active() THEN
RETURN NULL;
END IF;
IF TG_OP <> 'INSERT' THEN
old_channel_id := OLD.channel_id;
old_user_id := OLD.user_id;
PERFORM public.telesrv_bump_read_model_version('channel_member', old_user_id, 'channel', old_channel_id);
PERFORM public.telesrv_bump_dialog_light(old_user_id, 'channel', old_channel_id);
END IF;
IF TG_OP <> 'DELETE' THEN
new_channel_id := NEW.channel_id;
new_user_id := NEW.user_id;
IF TG_OP = 'INSERT'
OR (new_user_id, new_channel_id) IS DISTINCT FROM (old_user_id, old_channel_id)
THEN
PERFORM public.telesrv_bump_read_model_version('channel_member', new_user_id, 'channel', new_channel_id);
PERFORM public.telesrv_bump_dialog_light(new_user_id, 'channel', new_channel_id);
END IF;
END IF;
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_participants_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
old_channel_id bigint;
new_channel_id bigint;
BEGIN
IF public.telesrv_membership_batch_active() THEN
RETURN NULL;
END IF;
IF TG_OP = 'DELETE' THEN
old_channel_id := OLD.channel_id;
ELSIF TG_OP = 'INSERT' THEN
new_channel_id := NEW.channel_id;
ELSE
old_channel_id := OLD.channel_id;
new_channel_id := NEW.channel_id;
END IF;
IF old_channel_id IS NOT NULL THEN
PERFORM public.telesrv_bump_read_model_version('channel_participants', 0, 'channel', old_channel_id);
END IF;
IF new_channel_id IS NOT NULL AND new_channel_id IS DISTINCT FROM old_channel_id THEN
PERFORM public.telesrv_bump_read_model_version('channel_participants', 0, 'channel', new_channel_id);
END IF;
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_dialog_light_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
old_owner_id bigint;
old_channel_id bigint;
new_owner_id bigint;
new_channel_id bigint;
BEGIN
IF public.telesrv_membership_batch_active() THEN
RETURN NULL;
END IF;
IF TG_OP <> 'INSERT' THEN
old_owner_id := OLD.user_id;
old_channel_id := OLD.channel_id;
PERFORM public.telesrv_bump_dialog_light(old_owner_id, 'channel', old_channel_id);
END IF;
IF TG_OP <> 'DELETE' THEN
new_owner_id := NEW.user_id;
new_channel_id := NEW.channel_id;
IF TG_OP = 'INSERT'
OR (new_owner_id, new_channel_id) IS DISTINCT FROM (old_owner_id, old_channel_id)
THEN
PERFORM public.telesrv_bump_dialog_light(new_owner_id, 'channel', new_channel_id);
END IF;
END IF;
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_active_memberships_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
old_owner bigint;
new_owner bigint;
changed boolean;
BEGIN
IF public.telesrv_membership_batch_active() THEN
RETURN NULL;
END IF;
IF TG_OP = 'DELETE' THEN
old_owner := OLD.user_id;
IF old_owner <> 0 THEN
PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', old_owner, 'user', old_owner);
END IF;
RETURN NULL;
END IF;
new_owner := NEW.user_id;
IF TG_OP = 'INSERT' THEN
IF new_owner <> 0 THEN
PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', new_owner, 'user', new_owner);
END IF;
RETURN NULL;
END IF;
old_owner := OLD.user_id;
changed :=
OLD.user_id IS DISTINCT FROM NEW.user_id OR
OLD.channel_id IS DISTINCT FROM NEW.channel_id OR
OLD.status IS DISTINCT FROM NEW.status OR
OLD.deleted IS DISTINCT FROM NEW.deleted;
IF changed THEN
IF old_owner <> 0 THEN
PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', old_owner, 'user', old_owner);
END IF;
IF new_owner <> 0 AND new_owner IS DISTINCT FROM old_owner THEN
PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', new_owner, 'user', new_owner);
END IF;
END IF;
RETURN NULL;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_bump_channel_membership_read_models(
p_channel_id bigint,
p_user_ids bigint[]
)
RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
changed record;
BEGIN
IF COALESCE(p_channel_id, 0) <= 0 OR COALESCE(cardinality(p_user_ids), 0) = 0 THEN
RETURN;
END IF;
FOR changed IN
WITH requested AS MATERIALIZED (
SELECT DISTINCT user_id
FROM unnest(p_user_ids) AS input(user_id)
WHERE user_id > 0
), keys AS MATERIALIZED (
SELECT 10 AS priority, 'channel_participants'::text AS model, 0::bigint AS owner_user_id,
'channel'::text AS peer_type, p_channel_id AS peer_id
UNION
SELECT 20, 'channel_member', user_id, 'channel', p_channel_id FROM requested
UNION
SELECT 30, 'dialog_light', user_id, 'channel', p_channel_id FROM requested
UNION
SELECT 40, 'dialog_owner', user_id, 'user', user_id FROM requested
UNION
SELECT 50, 'channel_active_memberships', user_id, 'user', user_id FROM requested
), bumped AS (
INSERT INTO public.read_model_versions (
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
)
SELECT model, owner_user_id, peer_type, peer_id, 1,
public.telesrv_random_read_model_hash(), now()
FROM keys
ORDER BY priority, owner_user_id, peer_type, peer_id
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO UPDATE SET
version = public.read_model_versions.version + 1,
hash = public.telesrv_random_read_model_hash(),
updated_at = EXCLUDED.updated_at
RETURNING model, owner_user_id, peer_type, peer_id, version, hash
)
SELECT model, owner_user_id, peer_type, peer_id, version, hash
FROM bumped
ORDER BY model, owner_user_id, peer_type, peer_id
LOOP
PERFORM pg_notify(
'telesrv_read_model_changed',
json_build_object(
'model', changed.model,
'owner_user_id', changed.owner_user_id,
'peer_type', changed.peer_type,
'peer_id', changed.peer_id,
'version', changed.version,
'hash', changed.hash
)::text
);
END LOOP;
END;
$$;

View file

@ -0,0 +1,62 @@
CREATE OR REPLACE FUNCTION public.telesrv_bump_channel_membership_read_models(
p_channel_id bigint,
p_user_ids bigint[]
)
RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
changed record;
BEGIN
IF COALESCE(p_channel_id, 0) <= 0 OR COALESCE(cardinality(p_user_ids), 0) = 0 THEN
RETURN;
END IF;
FOR changed IN
WITH requested AS MATERIALIZED (
SELECT DISTINCT user_id
FROM unnest(p_user_ids) AS input(user_id)
WHERE user_id > 0
), keys AS MATERIALIZED (
SELECT 'channel_participants'::text AS model, 0::bigint AS owner_user_id,
'channel'::text AS peer_type, p_channel_id AS peer_id
UNION
SELECT 'channel_member', user_id, 'channel', p_channel_id FROM requested
UNION
SELECT 'dialog_light', user_id, 'channel', p_channel_id FROM requested
UNION
SELECT 'dialog_owner', user_id, 'user', user_id FROM requested
UNION
SELECT 'channel_active_memberships', user_id, 'user', user_id FROM requested
), bumped AS (
INSERT INTO public.read_model_versions (
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
)
SELECT model, owner_user_id, peer_type, peer_id, 1,
public.telesrv_random_read_model_hash(), now()
FROM keys
ORDER BY model, owner_user_id, peer_type, peer_id
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO UPDATE SET
version = public.read_model_versions.version + 1,
hash = public.telesrv_random_read_model_hash(),
updated_at = EXCLUDED.updated_at
RETURNING model, owner_user_id, peer_type, peer_id, version, hash
)
SELECT model, owner_user_id, peer_type, peer_id, version, hash
FROM bumped
ORDER BY model, owner_user_id, peer_type, peer_id
LOOP
PERFORM pg_notify(
'telesrv_read_model_changed',
json_build_object(
'model', changed.model,
'owner_user_id', changed.owner_user_id,
'peer_type', changed.peer_type,
'peer_id', changed.peer_id,
'version', changed.version,
'hash', changed.hash
)::text
);
END LOOP;
END;
$$;

View file

@ -0,0 +1,68 @@
-- 20260901000017 initially ordered version keys lexically, placing
-- channel_active_memberships before the member/dialog keys. Ordinary single-row
-- membership writes acquire those keys in the opposite order, so a concurrent
-- createChannel and batch invite could deadlock. Use the canonical physical
-- mutation order: participants -> member -> dialog -> owner -> active index.
CREATE OR REPLACE FUNCTION public.telesrv_bump_channel_membership_read_models(
p_channel_id bigint,
p_user_ids bigint[]
)
RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
changed record;
BEGIN
IF COALESCE(p_channel_id, 0) <= 0 OR COALESCE(cardinality(p_user_ids), 0) = 0 THEN
RETURN;
END IF;
FOR changed IN
WITH requested AS MATERIALIZED (
SELECT DISTINCT user_id
FROM unnest(p_user_ids) AS input(user_id)
WHERE user_id > 0
), keys AS MATERIALIZED (
SELECT 10 AS priority, 'channel_participants'::text AS model, 0::bigint AS owner_user_id,
'channel'::text AS peer_type, p_channel_id AS peer_id
UNION
SELECT 20, 'channel_member', user_id, 'channel', p_channel_id FROM requested
UNION
SELECT 30, 'dialog_light', user_id, 'channel', p_channel_id FROM requested
UNION
SELECT 40, 'dialog_owner', user_id, 'user', user_id FROM requested
UNION
SELECT 50, 'channel_active_memberships', user_id, 'user', user_id FROM requested
), bumped AS (
INSERT INTO public.read_model_versions (
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
)
SELECT model, owner_user_id, peer_type, peer_id, 1,
public.telesrv_random_read_model_hash(), now()
FROM keys
ORDER BY priority, owner_user_id, peer_type, peer_id
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO UPDATE SET
version = public.read_model_versions.version + 1,
hash = public.telesrv_random_read_model_hash(),
updated_at = EXCLUDED.updated_at
RETURNING model, owner_user_id, peer_type, peer_id, version, hash
)
SELECT model, owner_user_id, peer_type, peer_id, version, hash
FROM bumped
ORDER BY model, owner_user_id, peer_type, peer_id
LOOP
PERFORM pg_notify(
'telesrv_read_model_changed',
json_build_object(
'model', changed.model,
'owner_user_id', changed.owner_user_id,
'peer_type', changed.peer_type,
'peer_id', changed.peer_id,
'version', changed.version,
'hash', changed.hash
)::text
);
END LOOP;
END;
$$;

View file

@ -0,0 +1,7 @@
DROP TRIGGER IF EXISTS channels_peer_identity_created ON public.channels;
DROP TRIGGER IF EXISTS users_peer_identity_created ON public.users;
DROP FUNCTION IF EXISTS public.telesrv_seed_peer_identity_on_insert();
-- Backfilled read_model_versions rows are intentionally retained. 20260901000014 defines
-- one token for every durable peer; deleting them would violate the older
-- migration's contract after rollback and make negative facts uncacheable.

View file

@ -0,0 +1,68 @@
-- 20260901000014 seeded only the peers that existed when that migration ran. Real account
-- provisioning and channel creation after deployment therefore had no durable
-- token, so their empty username/verification facts were deliberately
-- uncacheable. Maintain the token at the peer lifecycle boundary instead.
CREATE OR REPLACE FUNCTION public.telesrv_seed_peer_identity_on_insert()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_TABLE_NAME = 'users' THEN
PERFORM public.telesrv_bump_peer_identity('user', NEW.id);
ELSIF TG_TABLE_NAME = 'channels' THEN
PERFORM public.telesrv_bump_peer_identity('channel', NEW.id);
ELSE
RAISE EXCEPTION 'unsupported peer identity seed table: %', TG_TABLE_NAME;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS users_peer_identity_created ON public.users;
CREATE TRIGGER users_peer_identity_created
AFTER INSERT ON public.users
FOR EACH ROW EXECUTE FUNCTION public.telesrv_seed_peer_identity_on_insert();
DROP TRIGGER IF EXISTS channels_peer_identity_created ON public.channels;
CREATE TRIGGER channels_peer_identity_created
AFTER INSERT ON public.channels
FOR EACH ROW EXECUTE FUNCTION public.telesrv_seed_peer_identity_on_insert();
-- Use the same bump helper rather than a silent INSERT so already-running
-- instances that cached a missing/zero token receive exact-key invalidation.
DO $$
DECLARE
peer_row record;
BEGIN
FOR peer_row IN
SELECT u.id
FROM public.users u
WHERE NOT EXISTS (
SELECT 1 FROM public.read_model_versions v
WHERE v.model = 'peer_identity'
AND v.owner_user_id = 0
AND v.peer_type = 'user'
AND v.peer_id = u.id
)
ORDER BY u.id
LOOP
PERFORM public.telesrv_bump_peer_identity('user', peer_row.id);
END LOOP;
FOR peer_row IN
SELECT c.id
FROM public.channels c
WHERE NOT EXISTS (
SELECT 1 FROM public.read_model_versions v
WHERE v.model = 'peer_identity'
AND v.owner_user_id = 0
AND v.peer_type = 'channel'
AND v.peer_id = c.id
)
ORDER BY c.id
LOOP
PERFORM public.telesrv_bump_peer_identity('channel', peer_row.id);
END LOOP;
END;
$$;

View file

@ -0,0 +1,8 @@
DROP FUNCTION IF EXISTS public.telesrv_bind_temp_auth_key(
bigint,
bigint,
bigint,
bigint,
integer,
bytea
);

View file

@ -0,0 +1,154 @@
-- Collapse the identity-sensitive auth.bindTempAuthKey write boundary into one
-- database call. The caller still owns the surrounding transaction/savepoint;
-- this function preserves the global identity -> raw temp -> permanent row
-- lock order shared with selector advance, revocation and direct deletion.
CREATE OR REPLACE FUNCTION public.telesrv_bind_temp_auth_key(
p_temp_auth_key_id bigint,
p_perm_auth_key_id bigint,
p_nonce bigint,
p_temp_session_id bigint,
p_expires_at integer,
p_encrypted_message bytea
)
RETURNS TABLE (
bind_status text,
merged_layer integer,
merged_observation_id bigint
)
LANGUAGE plpgsql
AS $$
DECLARE
v_temp_expiry integer;
v_temp_layer integer;
v_temp_observation_id bigint;
v_current_perm_id bigint;
v_perm_expiry integer;
v_perm_layer integer;
v_perm_observation_id bigint;
v_merged_layer integer;
v_merged_observation_id bigint;
v_updated integer;
BEGIN
IF p_expires_at <= 0 OR p_temp_auth_key_id = p_perm_auth_key_id THEN
RETURN QUERY SELECT 'binding_invalid'::text, 0, 0::bigint;
RETURN;
END IF;
PERFORM pg_advisory_xact_lock(
1096111176::integer,
hashint8(p_perm_auth_key_id)::integer
);
SELECT key.expires_at, key.layer, key.layer_observation_id
INTO v_temp_expiry, v_temp_layer, v_temp_observation_id
FROM public.auth_keys AS key
WHERE key.auth_key_id = p_temp_auth_key_id
FOR UPDATE;
IF NOT FOUND OR v_temp_expiry <= 0 OR v_temp_expiry <> p_expires_at THEN
RETURN QUERY SELECT 'binding_invalid'::text, 0, 0::bigint;
RETURN;
END IF;
SELECT binding.perm_auth_key_id
INTO v_current_perm_id
FROM public.temp_auth_key_bindings AS binding
WHERE binding.temp_auth_key_id = p_temp_auth_key_id;
IF FOUND AND v_current_perm_id <> p_perm_auth_key_id THEN
RETURN QUERY SELECT 'already_bound'::text, 0, 0::bigint;
RETURN;
END IF;
SELECT key.expires_at, key.layer, key.layer_observation_id
INTO v_perm_expiry, v_perm_layer, v_perm_observation_id
FROM public.auth_keys AS key
WHERE key.auth_key_id = p_perm_auth_key_id
FOR UPDATE;
IF NOT FOUND OR v_perm_expiry <> 0 THEN
RETURN QUERY SELECT 'binding_invalid'::text, 0, 0::bigint;
RETURN;
END IF;
IF v_temp_layer < 0
OR v_perm_layer < 0
OR v_temp_observation_id < 0
OR v_perm_observation_id < 0
OR (v_temp_observation_id > 0 AND v_temp_layer = 0)
OR (v_perm_observation_id > 0 AND v_perm_layer = 0)
THEN
RETURN QUERY SELECT 'layer_invalid'::text, 0, 0::bigint;
RETURN;
END IF;
IF v_temp_observation_id > v_perm_observation_id THEN
v_merged_layer := v_temp_layer;
v_merged_observation_id := v_temp_observation_id;
ELSIF v_perm_observation_id > v_temp_observation_id THEN
v_merged_layer := v_perm_layer;
v_merged_observation_id := v_perm_observation_id;
ELSIF v_temp_observation_id > 0 AND v_temp_layer <> v_perm_layer THEN
RETURN QUERY SELECT 'layer_conflict'::text, 0, 0::bigint;
RETURN;
ELSIF v_temp_observation_id > 0 THEN
v_merged_layer := v_temp_layer;
v_merged_observation_id := v_temp_observation_id;
ELSE
v_merged_layer := v_perm_layer;
v_merged_observation_id := 0;
END IF;
INSERT INTO public.temp_auth_key_bindings (
temp_auth_key_id,
perm_auth_key_id,
nonce,
temp_session_id,
expires_at,
encrypted_message
) VALUES (
p_temp_auth_key_id,
p_perm_auth_key_id,
p_nonce,
p_temp_session_id,
p_expires_at,
p_encrypted_message
)
ON CONFLICT (temp_auth_key_id) DO UPDATE SET
nonce = EXCLUDED.nonce,
temp_session_id = EXCLUDED.temp_session_id,
expires_at = EXCLUDED.expires_at,
encrypted_message = EXCLUDED.encrypted_message,
created_at = now()
WHERE public.temp_auth_key_bindings.perm_auth_key_id = EXCLUDED.perm_auth_key_id;
GET DIAGNOSTICS v_updated = ROW_COUNT;
IF v_updated <> 1 THEN
RETURN QUERY SELECT 'binding_invalid'::text, 0, 0::bigint;
RETURN;
END IF;
UPDATE public.auth_keys AS key
SET layer = v_merged_layer,
layer_observation_id = v_merged_observation_id
WHERE key.auth_key_id = ANY(
ARRAY[p_temp_auth_key_id, p_perm_auth_key_id]::bigint[]
);
GET DIAGNOSTICS v_updated = ROW_COUNT;
IF v_updated <> 2 THEN
RAISE EXCEPTION
'merge bound auth key Layer defaults updated % of 2 locked keys',
v_updated
USING ERRCODE = '23000';
END IF;
UPDATE public.authorizations AS authz
SET layer = v_merged_layer
WHERE authz.auth_key_id = ANY(
ARRAY[p_temp_auth_key_id, p_perm_auth_key_id]::bigint[]
);
RETURN QUERY SELECT 'ok'::text, v_merged_layer, v_merged_observation_id;
END;
$$;
COMMENT ON FUNCTION public.telesrv_bind_temp_auth_key(
bigint, bigint, bigint, bigint, integer, bytea
) IS 'Atomically binds a temporary auth key and returns the committed Layer observation while preserving identity lock order';

View file

@ -0,0 +1 @@
DROP INDEX IF EXISTS public.bootstrap_update_jobs_pending_auth_idx;

View file

@ -0,0 +1,3 @@
CREATE INDEX IF NOT EXISTS bootstrap_update_jobs_pending_auth_idx
ON public.bootstrap_update_jobs (user_id, auth_key_id, id)
WHERE (status)::text = 'pending'::text;

View file

@ -0,0 +1,10 @@
DROP TRIGGER IF EXISTS channels_monoforum_active_membership_changed ON public.channels;
DROP FUNCTION IF EXISTS public.telesrv_notify_monoforum_channel_visibility_read_model();
DROP FUNCTION IF EXISTS public.telesrv_bump_monoforum_visibility_owners(bigint, bigint);
DROP TRIGGER IF EXISTS channel_members_monoforum_manager_visibility_changed ON public.channel_members;
DROP FUNCTION IF EXISTS public.telesrv_notify_monoforum_manager_visibility_read_model();
DROP TRIGGER IF EXISTS channel_messages_monoforum_active_membership_changed ON public.channel_messages;
DROP FUNCTION IF EXISTS public.telesrv_notify_monoforum_message_visibility_read_model();
DROP FUNCTION IF EXISTS public.telesrv_bump_channel_active_membership_owner(bigint);

View file

@ -0,0 +1,194 @@
-- Extend the owner-scoped channel_active_memberships generation to every
-- durable predicate used by ListActiveChannelIDsForUser. Ordinary memberships
-- are covered by user_channel_member_index; these triggers cover monoforum
-- subscriber message visibility, manager rights, and parent/mono enablement.
CREATE OR REPLACE FUNCTION public.telesrv_bump_channel_active_membership_owner(p_user_id bigint)
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
IF COALESCE(p_user_id, 0) <= 0 THEN
RETURN;
END IF;
PERFORM public.telesrv_bump_read_model_version(
'channel_active_memberships', p_user_id, 'user', p_user_id
);
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_monoforum_message_visibility_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
old_owner bigint := 0;
new_owner bigint := 0;
BEGIN
IF TG_OP <> 'INSERT'
AND OLD.saved_peer_type = 'user'
AND OLD.saved_peer_id > 0
AND NOT OLD.deleted
THEN
old_owner := OLD.saved_peer_id;
END IF;
IF TG_OP <> 'DELETE'
AND NEW.saved_peer_type = 'user'
AND NEW.saved_peer_id > 0
AND NOT NEW.deleted
THEN
new_owner := NEW.saved_peer_id;
END IF;
IF old_owner > 0 THEN
PERFORM public.telesrv_bump_channel_active_membership_owner(old_owner);
END IF;
IF new_owner > 0 AND new_owner IS DISTINCT FROM old_owner THEN
PERFORM public.telesrv_bump_channel_active_membership_owner(new_owner);
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER channel_messages_monoforum_active_membership_changed
AFTER INSERT OR DELETE OR UPDATE OF channel_id, saved_peer_type, saved_peer_id, deleted
ON public.channel_messages
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_monoforum_message_visibility_read_model();
CREATE OR REPLACE FUNCTION public.telesrv_notify_monoforum_manager_visibility_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF OLD.role IS NOT DISTINCT FROM NEW.role
AND OLD.admin_rights IS NOT DISTINCT FROM NEW.admin_rights
THEN
RETURN NULL;
END IF;
IF EXISTS (
SELECT 1
FROM public.channels AS parent
WHERE parent.id = NEW.channel_id
AND parent.linked_monoforum_id <> 0
AND parent.broadcast_messages_allowed
AND NOT parent.deleted
) THEN
PERFORM public.telesrv_bump_channel_active_membership_owner(NEW.user_id);
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER channel_members_monoforum_manager_visibility_changed
AFTER UPDATE OF role, admin_rights ON public.channel_members
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_monoforum_manager_visibility_read_model();
CREATE OR REPLACE FUNCTION public.telesrv_bump_monoforum_visibility_owners(
p_parent_id bigint,
p_monoforum_id bigint
)
RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
affected record;
BEGIN
IF COALESCE(p_parent_id, 0) <= 0 OR COALESCE(p_monoforum_id, 0) <= 0 THEN
RETURN;
END IF;
FOR affected IN
SELECT DISTINCT owner_id
FROM (
SELECT member.user_id AS owner_id
FROM public.user_channel_member_index AS member
WHERE member.channel_id = p_parent_id
AND member.status = 'active'
AND NOT member.deleted
AND member.role IN ('creator', 'admin')
UNION
SELECT message.saved_peer_id AS owner_id
FROM public.channel_messages AS message
WHERE message.channel_id = p_monoforum_id
AND message.saved_peer_type = 'user'
AND message.saved_peer_id > 0
AND NOT message.deleted
) AS owners
WHERE owner_id > 0
ORDER BY owner_id
LOOP
PERFORM public.telesrv_bump_channel_active_membership_owner(affected.owner_id);
END LOOP;
END;
$$;
CREATE OR REPLACE FUNCTION public.telesrv_notify_monoforum_channel_visibility_read_model()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
old_parent_id bigint := 0;
old_monoforum_id bigint := 0;
new_parent_id bigint := 0;
new_monoforum_id bigint := 0;
BEGIN
IF (OLD.broadcast_messages_allowed, OLD.linked_monoforum_id, OLD.deleted, OLD.monoforum)
IS NOT DISTINCT FROM
(NEW.broadcast_messages_allowed, NEW.linked_monoforum_id, NEW.deleted, NEW.monoforum)
THEN
RETURN NULL;
END IF;
IF OLD.linked_monoforum_id > 0 THEN
IF OLD.monoforum THEN
old_parent_id := OLD.linked_monoforum_id;
old_monoforum_id := OLD.id;
ELSE
old_parent_id := OLD.id;
old_monoforum_id := OLD.linked_monoforum_id;
END IF;
END IF;
IF NEW.linked_monoforum_id > 0 THEN
IF NEW.monoforum THEN
new_parent_id := NEW.linked_monoforum_id;
new_monoforum_id := NEW.id;
ELSE
new_parent_id := NEW.id;
new_monoforum_id := NEW.linked_monoforum_id;
END IF;
END IF;
IF old_parent_id > 0 THEN
PERFORM public.telesrv_bump_monoforum_visibility_owners(old_parent_id, old_monoforum_id);
END IF;
IF new_parent_id > 0
AND (new_parent_id, new_monoforum_id)
IS DISTINCT FROM (old_parent_id, old_monoforum_id)
THEN
PERFORM public.telesrv_bump_monoforum_visibility_owners(new_parent_id, new_monoforum_id);
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER channels_monoforum_active_membership_changed
AFTER UPDATE OF broadcast_messages_allowed, linked_monoforum_id, deleted, monoforum
ON public.channels
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_monoforum_channel_visibility_read_model();
-- Existing monoforum owners need a fresh generation before any shared page can
-- be populated under the expanded dependency contract.
DO $$
DECLARE
linked record;
BEGIN
FOR linked IN
SELECT parent.id AS parent_id, parent.linked_monoforum_id AS monoforum_id
FROM public.channels AS parent
WHERE parent.linked_monoforum_id > 0
LOOP
PERFORM public.telesrv_bump_monoforum_visibility_owners(
linked.parent_id, linked.monoforum_id
);
END LOOP;
END;
$$;

View file

@ -0,0 +1,9 @@
ALTER TABLE public.bot_verifier_settings
DROP CONSTRAINT bot_verifier_settings_default_description_check,
ADD CONSTRAINT bot_verifier_settings_default_description_check
CHECK (octet_length(default_description) <= 280);
ALTER TABLE public.custom_verification_requests
DROP CONSTRAINT custom_verification_requests_requested_description_check,
ADD CONSTRAINT custom_verification_requests_requested_description_check
CHECK (octet_length(requested_description) <= 280);

View file

@ -0,0 +1,9 @@
ALTER TABLE public.bot_verifier_settings
DROP CONSTRAINT bot_verifier_settings_default_description_check,
ADD CONSTRAINT bot_verifier_settings_default_description_check
CHECK (octet_length(default_description) <= 512);
ALTER TABLE public.custom_verification_requests
DROP CONSTRAINT custom_verification_requests_requested_description_check,
ADD CONSTRAINT custom_verification_requests_requested_description_check
CHECK (octet_length(requested_description) <= 512);

View file

@ -0,0 +1,11 @@
\set ON_ERROR_STOP on
-- main and v2 intentionally have independent migration histories. Keep them
-- in separate PostgreSQL databases even when they share the local container.
SELECT 'CREATE DATABASE telesrv_main OWNER telesrv'
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'telesrv_main')
\gexec
SELECT 'CREATE DATABASE telesrv_v2 OWNER telesrv'
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'telesrv_v2')
\gexec

View file

@ -0,0 +1,46 @@
{
"schema_version": 1,
"desktop": {
"win64": {
"stable": {
"build": 7000007,
"version": "7.0.7",
"file": "tx64upd7000007",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"size": 0,
"disabled": true
}
}
},
"apps": {
"android": {
"stable": {
"id": 2026080701,
"version": "12.9.1",
"url": "https://updates.example.test/apps/telesrv-12.9.1.apk",
"url_by_source": {
"com.android.vending": "https://play.google.com/store/apps/details?id=your.application.id"
},
"can_not_skip": false,
"notes": {
"en": "A new version is available.",
"ru": "Доступна новая версия приложения."
},
"disabled": true
}
},
"ios": {
"stable": {
"id": 2026080702,
"version": "12.9.1",
"url": "https://apps.apple.com/app/id0000000000",
"can_not_skip": false,
"notes": {
"en": "A new version is available in the App Store.",
"ru": "В App Store доступна новая версия приложения."
},
"disabled": true
}
}
}
}