merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
100
deploy/docker/.env.example
Normal file
100
deploy/docker/.env.example
Normal 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
|
||||
1
deploy/docker/assets/test-server-rsa.pem.b64
Normal file
1
deploy/docker/assets/test-server-rsa.pem.b64
Normal file
|
|
@ -0,0 +1 @@
|
|||
LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBeEYvLzBNMCsvNVB6Z2ROYWdUWCtKK2RKZ3I3NVpDVHVpRzhpNHg3WXdtSkYramlPCkdDam03WDdCTENhTWMxK2hPWllETDMrR3ZsZS9BS3lrVzFxb3VhQ0pNVngvSCsybDhMRlhMZWxaMlBMYXdUYjgKQTdCbFRxV3pMM2RiNUJ1Z01OV3ppTDlUdWhSOEluMWJ3S1kwN1FWcFI5aW41empBc0FHTEJrK21HdDBEblZ5TQpmMVhvcDJsTENGTm1tMEY0eWtjQWVhTENDSVBiR1dkZGxpTFk4eEVFaEk0R08ybDFVM2taTXdJT2RPbkFHSkZ0CmdVQW9UZStGSFI2RjFzOWFkQ1ZaQjF0ZUwvaGY5UitXbWVrSnd5Z1Z6ME1ZRUg3eTZVNDlUNDUrL1c3T0Y2WDYKZzBXMGoxdVNTcnNZNHFON3R3eGJUYWQ5emRHWjd5cys5ditQdVFJREFRQUJBb0lCQUFqdXFPOHhkczBmU0xNKwpEdDdUdXVUTHcyODhDcElBa0EwS3FSYVZuNXh2NWVqMHk1blR1blZSRDY1WGJvb003b04xREY0THVmQk1nM2FmClk3WjRFRGFwVTdRNEZkdzQ3aFJkcks1ODc4WkxmYUhPUTNaVGZyZ3VGMUZ3WjNDZnhSQ1RsOS8vZStwNTVnK1gKamlYY0tZb2lkZUI3dlY5cUdIR3BFRTdRTHFrSUVpbk1FV05hQjh1dGN0SDdUWGRXYTRweWZJR2lQckhNTjJ6SApoRjQwSWI3bkpBNmtodHpzTkNEU0Q5WG5ibEVORW9kMUU1Z1JzalE5ZkdzaGRCdHBEc1hyTEJGTDdLTUREb1FtCmN6cnQvS3hsWk1wYnRPZno2dWE1ZUtFQkJUdE51dG1WY3AxcTl5K2NRcFBaem5ZaVJUTCtPSnpWZ2Z1SE9PLzAKZWEyai93RUNnWUVBNkk2ZitFTG84QVA3UTRXMHFzc3VYS3ZIRUYvUkVFZngyTHVlaDBsclVoVlIzSWdqYWtGYwpiVFJsTEVRSzhRTmJEQW1OTXVIckVabmptR3U3bnkzek45NHNsQy95Y0k1cGQ5aFFHVndPMjJlcjlvMitNOXcyCkpwQjRRZjhjOFNLL3BIUU45K2pPdEJ4VjJkcmJmZTBvVW9LRXRZeUQ1Y2J0akZUM3lmZUEyQ0VDZ1lFQTJDdW8KUzg0MWtWcHB4MUt3cEw0aTFmZ0dRUHlrYUtyRDRvR1pyWWd2MkZ4VzU1aS9xNUZjWUw2ZkUzN0phTTFwby9SQgp0bkhnMzVOYW5nL3l1Wlh6NkViNEswQ3lMKzhMdWhDSWI2UHhEdnF5Q1hia1hUd0hTVlJBbkdFTVNOM3phLzdGCkZDSkYxQUJXZzhqbEtSZEEvbFJ3cW84UDhaZ0JlQk1xcEtaaDVKa0NnWUJoQ0ltazI3NDN6MkY2dGdKQk5VL2QKNk9yQllVbHBJcXU5ZytOTWpZelREZ1EvSVNxdHZpSGppdllmOXpBZGlnbm1SdUg4ZGhsUUdjYkdKVVYrMEh4bwpOaktoampQNVZPS2ExODNzRnVZNEU5VERwamJUaXJHcGU2UkIzVUZsTjl1QXNjL1dQZlJwWUYxTjdpeWhLV0FtCnRVRE1RNW9ST09TTEpqVFJ0NHl5SVFLQmdRQ1RmeTVsRXYyd0FQMzk5K2o1YjVhN1luRjU5Q2lHRmtaMERiUDcKR05wMGlZVHVuMlhndmQxSFVhbWZGcnA4blBRQTM4L2FtZGN6RmdzVm9KSWdtVFdFZnJBa2F3OXA3M1NUNzJYNApydWJ6THBFK0xmWmh1MnpKVndpQzZ5RURzeFc5MFdkTmRwa29yMVpZczBIUmlNRmJCK2ljSitOY0dEaWdZb3VOCkxzM0t1UUtCZ1FDVEF4Vk03ek5QZ3NmZnVkWlA3Rk1ueTIwTE1SNitGUW83eFJ6NUVsWWFDVmdYTTFOSDRLL3IKeUZrT0NHaE9ONkkvTVVOQlB4V0xFdVpCUmZ5cmx5M2JwM2o3UjYvaExYNGZOeXc5QjNHUXVWeUNIRDJENHRvZAo2SjVWby9Fc2VxcGVlS2E2Q3YvWTJIVkIwa2ZHdzFWQ0MvZ00wMUw4dWVkM2hzcjJMRDJrQ3c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
|
||||
8
deploy/docker/assets/test-server-rsa.pub
Normal file
8
deploy/docker/assets/test-server-rsa.pub
Normal 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-----
|
||||
63
deploy/docker/compose.bridge-network.yaml
Normal file
63
deploy/docker/compose.bridge-network.yaml
Normal 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
228
deploy/docker/compose.yaml
Normal 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:
|
||||
99
deploy/docker/docker-entrypoint.sh
Executable file
99
deploy/docker/docker-entrypoint.sh
Executable 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 "$@"
|
||||
|
|
@ -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: -
|
||||
--
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
DROP TABLE IF EXISTS broadcast_recipients;
|
||||
DROP TABLE IF EXISTS broadcasts;
|
||||
54
deploy/migrations/20260901000001_system_broadcasts.up.sql
Normal file
54
deploy/migrations/20260901000001_system_broadcasts.up.sql
Normal 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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE broadcasts
|
||||
DROP CONSTRAINT IF EXISTS broadcasts_entities_array_check,
|
||||
DROP COLUMN IF EXISTS entities;
|
||||
|
|
@ -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');
|
||||
|
|
@ -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();
|
||||
129
deploy/migrations/20260901000004_logical_account_deletion.up.sql
Normal file
129
deploy/migrations/20260901000004_logical_account_deletion.up.sql
Normal 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;
|
||||
$$;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
DROP TABLE IF EXISTS welcome_messages;
|
||||
DROP TABLE IF EXISTS welcome_message_peers;
|
||||
31
deploy/migrations/20260901000007_welcome_messages.up.sql
Normal file
31
deploy/migrations/20260901000007_welcome_messages.up.sql
Normal 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
|
||||
)
|
||||
);
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
DROP TABLE IF EXISTS welcome_message_deliveries;
|
||||
DROP SEQUENCE IF EXISTS welcome_message_join_event_id_seq;
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1 @@
|
|||
DROP INDEX IF EXISTS welcome_message_deliveries_target_idx;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
CREATE INDEX welcome_message_deliveries_target_idx
|
||||
ON welcome_message_deliveries (channel_id, target_user_id, id);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
DROP FUNCTION IF EXISTS public.telesrv_advance_auth_session_layer(
|
||||
bigint,
|
||||
bigint,
|
||||
integer,
|
||||
bigint,
|
||||
timestamptz
|
||||
);
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
|
|
@ -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.
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
|
|
@ -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;
|
||||
$$;
|
||||
|
|
@ -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;
|
||||
$$;
|
||||
|
|
@ -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;
|
||||
$$;
|
||||
|
|
@ -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.
|
||||
|
|
@ -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;
|
||||
$$;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
DROP FUNCTION IF EXISTS public.telesrv_bind_temp_auth_key(
|
||||
bigint,
|
||||
bigint,
|
||||
bigint,
|
||||
bigint,
|
||||
integer,
|
||||
bytea
|
||||
);
|
||||
|
|
@ -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';
|
||||
|
|
@ -0,0 +1 @@
|
|||
DROP INDEX IF EXISTS public.bootstrap_update_jobs_pending_auth_idx;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
$$;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
11
deploy/postgres-init/010_branch_databases.sql
Normal file
11
deploy/postgres-init/010_branch_databases.sql
Normal 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
|
||||
46
deploy/update/manifest.example.json
Normal file
46
deploy/update/manifest.example.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue