Initial open source release
This commit is contained in:
commit
74992e893f
377 changed files with 118084 additions and 0 deletions
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Go build artifacts
|
||||
/bin/
|
||||
*.exe
|
||||
*.test
|
||||
*.out
|
||||
coverage.*
|
||||
|
||||
# 本地环境 / 密钥(server RSA private key 必须持久化,但禁止入库)
|
||||
*.local
|
||||
*.pem
|
||||
*.key
|
||||
/secrets/
|
||||
/data/*
|
||||
!/data/langpack/
|
||||
!/data/langpack/**
|
||||
logs/
|
||||
.gocache/
|
||||
.tdesktop-e2e/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
195
README.md
Normal file
195
README.md
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
# telesrv
|
||||
|
||||
`telesrv` is a Telegram-like MTProto server written in Go. It uses
|
||||
[`github.com/gotd/td`](https://github.com/gotd/td) v0.144.0 / Layer 225 as the TL
|
||||
and MTProto base, and its first compatibility target is a pinned Telegram
|
||||
Desktop build.
|
||||
|
||||
`telesrv` is an independent, unofficial project. It is not affiliated with,
|
||||
endorsed by, or sponsored by Telegram or the official Telegram team.
|
||||
|
||||
[中文 README](README.zh-CN.md)
|
||||
|
||||

|
||||
|
||||
## Status
|
||||
|
||||
This project is useful for local protocol research and Telegram Desktop
|
||||
compatibility work. It is not a production Telegram replacement.
|
||||
|
||||
Implemented main paths include MTProto key exchange, login with a development
|
||||
code, users/contacts/dialogs, private messages, supergroups/channels, update
|
||||
difference recovery, local media/files, profile/channel photos, stickers,
|
||||
reactions, and presence. Large-scale public channels, multi-DC/file-DC/CDN,
|
||||
Bot API, payments, stories, Premium business logic, production abuse controls,
|
||||
and production object storage are intentionally out of scope for now.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are very welcome. The most helpful areas right now are Telegram
|
||||
Desktop compatibility reports, reproducible RPC traces, focused bug fixes,
|
||||
tests for online/offline update behavior, performance work on already
|
||||
implemented paths, and documentation that makes local setup easier.
|
||||
|
||||
Please keep changes scoped and compatibility-driven. If a change affects
|
||||
Telegram Desktop behavior, include the client version/commit, the RPC path you
|
||||
tested, and whether server logs stayed free of `NOT_IMPLEMENTED`, `Unhandled
|
||||
RPC`, `bad_msg`, panic, or internal errors.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
```text
|
||||
cmd/telesrv/ server entrypoint
|
||||
deploy/ docker-compose and PostgreSQL migrations
|
||||
internal/mtprotoedge/ MTProto transport, auth key, session, ack/resend
|
||||
internal/rpc/ TL router and Telegram Desktop compatibility handlers
|
||||
internal/app/ domain services
|
||||
internal/domain/ protocol-independent domain models
|
||||
internal/store/ store interfaces and memory/postgres/redis backends
|
||||
docs/ compatibility notes and module design docs
|
||||
```
|
||||
|
||||
## Run telesrv
|
||||
|
||||
Requirements:
|
||||
|
||||
- Go 1.25 or newer
|
||||
- Docker Desktop or Docker Engine with Compose
|
||||
- OpenSSL, if you want to build a matching Telegram Desktop client
|
||||
|
||||
Start PostgreSQL and Redis:
|
||||
|
||||
```powershell
|
||||
docker compose -f deploy/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
Build and run the server:
|
||||
|
||||
```powershell
|
||||
go build -o bin/telesrv.exe ./cmd/telesrv
|
||||
.\bin\telesrv.exe
|
||||
```
|
||||
|
||||
On first start, `telesrv` creates `data/server_rsa.pem`, applies all database
|
||||
migrations, seeds bundled language packs, and listens on `0.0.0.0:2398`.
|
||||
|
||||
Useful development environment variables:
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|---|---:|---|
|
||||
| `TELESRV_LISTEN` | `0.0.0.0:2398` | MTProto listen address |
|
||||
| `TELESRV_ADVERTISE_IP` | `127.0.0.1` | IP written into `help.getConfig` |
|
||||
| `TELESRV_DC` | `2` | self-hosted DC id |
|
||||
| `TELESRV_DEV_AUTH_CODE` | `12345` | fixed login code for local development |
|
||||
| `TELESRV_POSTGRES_DSN` | local Compose DSN | PostgreSQL connection string |
|
||||
| `TELESRV_REDIS_ADDR` | `localhost:6399` | Redis address |
|
||||
| `TELESRV_STICKER_SEED_DIR` | `data/sticker-seed` | optional exported sticker/reaction seed directory |
|
||||
|
||||
The optional sticker seed directory is skipped when it does not exist.
|
||||
|
||||
## Build Telegram Desktop For telesrv
|
||||
|
||||
The stock Telegram Desktop binary will not connect to `telesrv`: it trusts
|
||||
Telegram's production DC list and RSA keys. Build your own patched client.
|
||||
|
||||
Target baseline:
|
||||
|
||||
- Telegram Desktop commit: `9caf32dffc90ddd9bb08ad5777b865f729fa167b`
|
||||
- TL layer: 225
|
||||
- Local DC: `127.0.0.1:2398`, DC id `2`
|
||||
|
||||
Clone and pin Telegram Desktop:
|
||||
|
||||
```powershell
|
||||
git clone --recursive https://github.com/telegramdesktop/tdesktop.git
|
||||
cd tdesktop
|
||||
git checkout 9caf32dffc90ddd9bb08ad5777b865f729fa167b
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
Build prerequisites and exact platform instructions are maintained upstream:
|
||||
|
||||
- Windows: `docs/building-win.md`
|
||||
- macOS: `docs/building-mac.md`
|
||||
- Linux: `docs/building-linux.md`
|
||||
|
||||
For Windows x64, the pinned upstream instructions currently boil down to:
|
||||
|
||||
```powershell
|
||||
Telegram\build\prepare\win.bat
|
||||
cd Telegram
|
||||
configure.bat x64 -D TDESKTOP_API_ID=YOUR_API_ID -D TDESKTOP_API_HASH=YOUR_API_HASH
|
||||
```
|
||||
|
||||
Then open `out\Telegram.slnx` in Visual Studio and build the `Telegram`
|
||||
project. The debug binary is written to `out\Debug\Telegram.exe`.
|
||||
|
||||
## Patch Telegram Desktop
|
||||
|
||||
After `telesrv` has generated `data/server_rsa.pem`, export the matching public
|
||||
key:
|
||||
|
||||
```powershell
|
||||
openssl rsa -in data/server_rsa.pem -RSAPublicKey_out -out data/server_rsa.pub
|
||||
```
|
||||
|
||||
Patch Telegram Desktop file
|
||||
`Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp`:
|
||||
|
||||
1. Replace built-in production and test DC lists with local DC 2:
|
||||
|
||||
```cpp
|
||||
const BuiltInDc kBuiltInDcs[] = {
|
||||
{ 2, "127.0.0.1", 2398 },
|
||||
};
|
||||
|
||||
const BuiltInDc kBuiltInDcsIPv6[] = {
|
||||
{ 2, "::1", 2398 },
|
||||
};
|
||||
|
||||
const BuiltInDc kBuiltInDcsTest[] = {
|
||||
{ 2, "127.0.0.1", 2398 },
|
||||
};
|
||||
|
||||
const BuiltInDc kBuiltInDcsIPv6Test[] = {
|
||||
{ 2, "::1", 2398 },
|
||||
};
|
||||
```
|
||||
|
||||
2. Replace both `kPublicRSAKeys` and `kTestPublicRSAKeys` with the contents of
|
||||
`data/server_rsa.pub`.
|
||||
3. In `DcOptions::constructFromBuiltIn()`, add `Flag::f_tcpo_only` to the IPv4
|
||||
and IPv6 built-in DC flags.
|
||||
|
||||
Keep this client patch minimal: DC endpoints, RSA key, and TCP-only flags only.
|
||||
Do not mix UI changes into the protocol patch.
|
||||
|
||||
## Run Two Local Desktop Clients
|
||||
|
||||
Use separate TDesktop working directories so Alice and Bob do not share `tdata`:
|
||||
|
||||
```powershell
|
||||
$tdesktop = "C:\path\to\tdesktop\out\Debug\Telegram.exe"
|
||||
Start-Process $tdesktop -ArgumentList @("-workdir", "$PWD\.tdata-alice")
|
||||
Start-Process $tdesktop -ArgumentList @("-workdir", "$PWD\.tdata-bob")
|
||||
```
|
||||
|
||||
Log in with two different phone numbers. In local development, the login code is
|
||||
`12345` unless you changed `TELESRV_DEV_AUTH_CODE`.
|
||||
|
||||
If the client keeps reconnecting, check these first:
|
||||
|
||||
- `telesrv` is listening on port `2398`.
|
||||
- `data/server_rsa.pub` was copied into both RSA key arrays in TDesktop.
|
||||
- `TELESRV_ADVERTISE_IP` matches the address reachable from the client.
|
||||
- TDesktop was built from the pinned Layer 225 baseline or re-audited for a new
|
||||
layer.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Compatibility matrix](docs/compatibility-matrix.md)
|
||||
- [Telegram Desktop patch notes](docs/tdesktop-patch-notes.md)
|
||||
- [Persistence layer](docs/persistence-layer.md)
|
||||
- [Message module](docs/message-module.md)
|
||||
- [Channel module](docs/channel-module.md)
|
||||
- [Performance audit](docs/performance-audit.md)
|
||||
182
README.zh-CN.md
Normal file
182
README.zh-CN.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
# telesrv
|
||||
|
||||
`telesrv` 是一个用 Go 编写的 Telegram-like MTProto server。它以
|
||||
[`github.com/gotd/td`](https://github.com/gotd/td) v0.144.0 / Layer 225 作为
|
||||
TL 与 MTProto 基础,第一兼容目标是固定基线的 Telegram Desktop。
|
||||
|
||||
`telesrv` 是独立的非官方项目,与 Telegram 官方及其团队没有关联,也未获得其背书或赞助。
|
||||
|
||||
[English README](README.md)
|
||||
|
||||

|
||||
|
||||
## 当前状态
|
||||
|
||||
本项目适合本地协议研究、Telegram Desktop 兼容性验证和自建 MTProto server 实验。它不是生产级 Telegram 替代品。
|
||||
|
||||
当前已覆盖的主路径包括 MTProto key exchange、开发验证码登录、users/contacts/dialogs、私聊消息、超级群/频道、updates difference 恢复、本地 media/files、用户/频道头像、stickers、reactions 和 presence。
|
||||
|
||||
暂不默认覆盖大规模公开频道、多 DC / 文件 DC / CDN、Bot API、payments、stories、Premium 商业逻辑、生产风控、生产对象存储等能力。
|
||||
|
||||
## 欢迎贡献
|
||||
|
||||
欢迎大家参与贡献。现在最有价值的方向包括 Telegram Desktop 兼容性报告、可复现 RPC trace、聚焦的小 bug fix、在线/离线 updates 行为测试、已实现路径的性能优化,以及让本地启动更顺滑的文档改进。
|
||||
|
||||
请尽量保持改动范围清晰,并围绕兼容性目标展开。如果改动会影响 Telegram Desktop 可见行为,请在 PR 或说明里写清客户端版本/commit、验证过的 RPC 路径,以及 server 日志是否没有新增 `NOT_IMPLEMENTED`、`Unhandled RPC`、`bad_msg`、panic 或 internal error。
|
||||
|
||||
## 仓库结构
|
||||
|
||||
```text
|
||||
cmd/telesrv/ server 启动入口
|
||||
deploy/ docker-compose 与 PostgreSQL migrations
|
||||
internal/mtprotoedge/ MTProto transport、auth key、session、ack/resend
|
||||
internal/rpc/ TL router 与 Telegram Desktop 兼容 handlers
|
||||
internal/app/ domain services
|
||||
internal/domain/ 不依赖协议生成类型的 domain models
|
||||
internal/store/ store interfaces 与 memory/postgres/redis 后端
|
||||
docs/ 兼容性记录与模块设计文档
|
||||
```
|
||||
|
||||
## 运行 telesrv
|
||||
|
||||
依赖:
|
||||
|
||||
- Go 1.25 或更新版本
|
||||
- Docker Desktop 或带 Compose 的 Docker Engine
|
||||
- OpenSSL,如果要编译匹配的 Telegram Desktop 客户端
|
||||
|
||||
启动 PostgreSQL 和 Redis:
|
||||
|
||||
```powershell
|
||||
docker compose -f deploy/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
编译并启动 server:
|
||||
|
||||
```powershell
|
||||
go build -o bin/telesrv.exe ./cmd/telesrv
|
||||
.\bin\telesrv.exe
|
||||
```
|
||||
|
||||
第一次启动时,`telesrv` 会创建 `data/server_rsa.pem`,自动执行所有数据库 migrations,导入内置语言包,并监听 `0.0.0.0:2398`。
|
||||
|
||||
常用开发环境变量:
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|---|---:|---|
|
||||
| `TELESRV_LISTEN` | `0.0.0.0:2398` | MTProto 监听地址 |
|
||||
| `TELESRV_ADVERTISE_IP` | `127.0.0.1` | 写入 `help.getConfig` 的客户端连接 IP |
|
||||
| `TELESRV_DC` | `2` | 自建 DC id |
|
||||
| `TELESRV_DEV_AUTH_CODE` | `12345` | 本地开发固定登录验证码 |
|
||||
| `TELESRV_POSTGRES_DSN` | local Compose DSN | PostgreSQL 连接串 |
|
||||
| `TELESRV_REDIS_ADDR` | `localhost:6399` | Redis 地址 |
|
||||
| `TELESRV_STICKER_SEED_DIR` | `data/sticker-seed` | 可选的 sticker/reaction 导出种子目录 |
|
||||
|
||||
如果 sticker seed 目录不存在,启动时会自动跳过。
|
||||
|
||||
## 编译连接 telesrv 的 Telegram Desktop
|
||||
|
||||
官方 Telegram Desktop 二进制不能直接连接 `telesrv`,因为它信任的是 Telegram 官方 DC 列表和 RSA keys。你需要编译一个最小 patch 过的客户端。
|
||||
|
||||
目标基线:
|
||||
|
||||
- Telegram Desktop commit:`9caf32dffc90ddd9bb08ad5777b865f729fa167b`
|
||||
- TL layer:225
|
||||
- 本地 DC:`127.0.0.1:2398`,DC id `2`
|
||||
|
||||
克隆并固定 Telegram Desktop:
|
||||
|
||||
```powershell
|
||||
git clone --recursive https://github.com/telegramdesktop/tdesktop.git
|
||||
cd tdesktop
|
||||
git checkout 9caf32dffc90ddd9bb08ad5777b865f729fa167b
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
编译依赖和各平台完整说明以 Telegram Desktop 上游文档为准:
|
||||
|
||||
- Windows:`docs/building-win.md`
|
||||
- macOS:`docs/building-mac.md`
|
||||
- Linux:`docs/building-linux.md`
|
||||
|
||||
Windows x64 下,固定基线的主要步骤是:
|
||||
|
||||
```powershell
|
||||
Telegram\build\prepare\win.bat
|
||||
cd Telegram
|
||||
configure.bat x64 -D TDESKTOP_API_ID=YOUR_API_ID -D TDESKTOP_API_HASH=YOUR_API_HASH
|
||||
```
|
||||
|
||||
然后用 Visual Studio 打开 `out\Telegram.slnx`,构建 `Telegram` project。Debug 二进制会生成在 `out\Debug\Telegram.exe`。
|
||||
|
||||
## Patch Telegram Desktop
|
||||
|
||||
等 `telesrv` 生成 `data/server_rsa.pem` 后,导出匹配的公钥:
|
||||
|
||||
```powershell
|
||||
openssl rsa -in data/server_rsa.pem -RSAPublicKey_out -out data/server_rsa.pub
|
||||
```
|
||||
|
||||
修改 Telegram Desktop 文件:
|
||||
|
||||
```text
|
||||
Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp
|
||||
```
|
||||
|
||||
1. 把内置 production/test DC 列表替换为本地 DC 2:
|
||||
|
||||
```cpp
|
||||
const BuiltInDc kBuiltInDcs[] = {
|
||||
{ 2, "127.0.0.1", 2398 },
|
||||
};
|
||||
|
||||
const BuiltInDc kBuiltInDcsIPv6[] = {
|
||||
{ 2, "::1", 2398 },
|
||||
};
|
||||
|
||||
const BuiltInDc kBuiltInDcsTest[] = {
|
||||
{ 2, "127.0.0.1", 2398 },
|
||||
};
|
||||
|
||||
const BuiltInDc kBuiltInDcsIPv6Test[] = {
|
||||
{ 2, "::1", 2398 },
|
||||
};
|
||||
```
|
||||
|
||||
2. 把 `kPublicRSAKeys` 和 `kTestPublicRSAKeys` 都替换为 `data/server_rsa.pub` 的内容。
|
||||
3. 在 `DcOptions::constructFromBuiltIn()` 中给 IPv4 与 IPv6 built-in DC flags 加上 `Flag::f_tcpo_only`。
|
||||
|
||||
```cpp
|
||||
const auto flags = Flag::f_static | Flag::f_tcpo_only;
|
||||
const auto flags = Flag::f_static | Flag::f_ipv6 | Flag::f_tcpo_only;
|
||||
```
|
||||
|
||||
客户端 patch 应保持最小:只改 DC endpoint、RSA public key 和 TCP-only flags,不要把 UI 改动混入协议兼容 patch。
|
||||
|
||||
## 启动两个本地 Desktop 客户端
|
||||
|
||||
用不同的 TDesktop working directory,避免 Alice 和 Bob 共用同一个 `tdata`:
|
||||
|
||||
```powershell
|
||||
$tdesktop = "C:\path\to\tdesktop\out\Debug\Telegram.exe"
|
||||
Start-Process $tdesktop -ArgumentList @("-workdir", "$PWD\.tdata-alice")
|
||||
Start-Process $tdesktop -ArgumentList @("-workdir", "$PWD\.tdata-bob")
|
||||
```
|
||||
|
||||
用两个不同手机号登录。本地开发默认验证码是 `12345`,除非你修改了 `TELESRV_DEV_AUTH_CODE`。
|
||||
|
||||
如果客户端一直重连,优先检查:
|
||||
|
||||
- `telesrv` 是否正在监听 `2398`。
|
||||
- `data/server_rsa.pub` 是否同时复制到了 TDesktop 的两个 RSA key 数组。
|
||||
- `TELESRV_ADVERTISE_IP` 是否是客户端可访问的地址。
|
||||
- TDesktop 是否基于固定 Layer 225 基线构建,或者你已经重新审计了新 layer。
|
||||
|
||||
## 文档
|
||||
|
||||
- [兼容矩阵](docs/compatibility-matrix.md)
|
||||
- [Telegram Desktop patch notes](docs/tdesktop-patch-notes.md)
|
||||
- [持久化层设计](docs/persistence-layer.md)
|
||||
- [消息模块](docs/message-module.md)
|
||||
- [频道模块](docs/channel-module.md)
|
||||
- [性能审计](docs/performance-audit.md)
|
||||
276
cmd/telesrv/exe_integration_test.go
Normal file
276
cmd/telesrv/exe_integration_test.go
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
tdcrypto "github.com/gotd/td/crypto"
|
||||
"github.com/gotd/td/exchange"
|
||||
"github.com/gotd/td/mtproxy"
|
||||
"github.com/gotd/td/mtproxy/obfuscator"
|
||||
"github.com/gotd/td/proto/codec"
|
||||
"github.com/gotd/td/session"
|
||||
"github.com/gotd/td/telegram"
|
||||
"github.com/gotd/td/telegram/dcs"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/gotd/td/transport"
|
||||
|
||||
"telesrv/internal/mtprotoedge"
|
||||
)
|
||||
|
||||
func TestExecutablePrivateMessageRoundTrip(t *testing.T) {
|
||||
exe := os.Getenv("TELESRV_TEST_EXE")
|
||||
if exe == "" {
|
||||
t.Skip("set TELESRV_TEST_EXE to run executable-level MTProto roundtrip")
|
||||
}
|
||||
repoRoot := filepath.Clean(filepath.Join("..", ".."))
|
||||
if !filepath.IsAbs(exe) {
|
||||
exe = filepath.Join(repoRoot, exe)
|
||||
}
|
||||
exeAbs, err := filepath.Abs(exe)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve executable path: %v", err)
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("reserve listen port: %v", err)
|
||||
}
|
||||
addr := ln.Addr().String()
|
||||
_ = ln.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd := exec.CommandContext(ctx, exeAbs)
|
||||
cmd.Dir = repoRoot
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Env = append(os.Environ(),
|
||||
"TELESRV_LISTEN="+addr,
|
||||
"TELESRV_ADVERTISE_IP=127.0.0.1",
|
||||
)
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("start executable: %v", err)
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- cmd.Wait() }()
|
||||
processDone := false
|
||||
t.Cleanup(func() {
|
||||
if processDone {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
default:
|
||||
_ = cmd.Process.Kill()
|
||||
<-done
|
||||
}
|
||||
})
|
||||
select {
|
||||
case err := <-done:
|
||||
processDone = true
|
||||
t.Fatalf("executable exited before client test: %v\nstdout:\n%s\nstderr:\n%s", err, stdout.String(), stderr.String())
|
||||
case <-time.After(4 * time.Second):
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("wait executable startup: %v\nstdout:\n%s\nstderr:\n%s", ctx.Err(), stdout.String(), stderr.String())
|
||||
}
|
||||
|
||||
rsaKey, err := mtprotoedge.LoadOrGenerateRSAKey(filepath.Join(repoRoot, "data", "server_rsa.pem"))
|
||||
if err != nil {
|
||||
t.Fatalf("load rsa key: %v", err)
|
||||
}
|
||||
host, portStr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
t.Fatalf("split addr: %v", err)
|
||||
}
|
||||
var port int
|
||||
if _, err := fmt.Sscanf(portStr, "%d", &port); err != nil {
|
||||
t.Fatalf("parse port %q: %v", portStr, err)
|
||||
}
|
||||
|
||||
newClient := func(storage *session.StorageMemory) *telegram.Client {
|
||||
opts := telegram.Options{
|
||||
PublicKeys: []exchange.PublicKey{{RSA: &rsaKey.PublicKey}},
|
||||
Resolver: noSecretObfuscatedResolver{addr: addr, dc: 2},
|
||||
DCList: dcs.List{Options: []tg.DCOption{{ID: 2, IPAddress: host, Port: port, Static: true}}},
|
||||
Logger: zaptest.NewLogger(t).Named("exe-client"),
|
||||
SessionStorage: storage,
|
||||
UpdateHandler: telegram.UpdateHandlerFunc(func(context.Context, tg.UpdatesClass) error { return nil }),
|
||||
}
|
||||
return telegram.NewClient(1, "hash", opts)
|
||||
}
|
||||
messagesOf := func(history tg.MessagesMessagesClass) []tg.MessageClass {
|
||||
t.Helper()
|
||||
switch v := history.(type) {
|
||||
case *tg.MessagesMessages:
|
||||
return v.Messages
|
||||
case *tg.MessagesMessagesSlice:
|
||||
return v.Messages
|
||||
default:
|
||||
t.Fatalf("history = %T %+v, want messages", history, history)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
signUp := func(storage *session.StorageMemory, phone, firstName string) tg.User {
|
||||
t.Helper()
|
||||
client := newClient(storage)
|
||||
var out tg.User
|
||||
if err := client.Run(ctx, func(ctx context.Context) error {
|
||||
raw := tg.NewClient(client)
|
||||
sent, err := raw.AuthSendCode(ctx, &tg.AuthSendCodeRequest{
|
||||
PhoneNumber: phone,
|
||||
APIID: 1,
|
||||
APIHash: "hash",
|
||||
Settings: tg.CodeSettings{},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash := sent.(*tg.AuthSentCode).PhoneCodeHash
|
||||
signInRes, err := raw.AuthSignIn(ctx, &tg.AuthSignInRequest{
|
||||
PhoneNumber: phone,
|
||||
PhoneCodeHash: hash,
|
||||
PhoneCode: "12345",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if authz, ok := signInRes.(*tg.AuthAuthorization); ok {
|
||||
out = *(authz.User.(*tg.User))
|
||||
return nil
|
||||
}
|
||||
res, err := raw.AuthSignUp(ctx, &tg.AuthSignUpRequest{
|
||||
PhoneNumber: phone,
|
||||
PhoneCodeHash: hash,
|
||||
FirstName: firstName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authz := res.(*tg.AuthAuthorization)
|
||||
out = *(authz.User.(*tg.User))
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatalf("sign up %s: %v\nstdout:\n%s\nstderr:\n%s", firstName, err, stdout.String(), stderr.String())
|
||||
}
|
||||
return out
|
||||
}
|
||||
sendAndAssertHistory := func(storage *session.StorageMemory, to tg.User, body string, randomID int64, wantOut bool) {
|
||||
t.Helper()
|
||||
client := newClient(storage)
|
||||
if err := client.Run(ctx, func(ctx context.Context) error {
|
||||
raw := tg.NewClient(client)
|
||||
if _, err := raw.MessagesSendMessage(ctx, &tg.MessagesSendMessageRequest{
|
||||
Peer: &tg.InputPeerUser{UserID: to.ID, AccessHash: to.AccessHash},
|
||||
Message: body,
|
||||
RandomID: randomID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
history, err := raw.MessagesGetHistory(ctx, &tg.MessagesGetHistoryRequest{
|
||||
Peer: &tg.InputPeerUser{UserID: to.ID, AccessHash: to.AccessHash},
|
||||
Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msgs := messagesOf(history)
|
||||
if len(msgs) == 0 {
|
||||
t.Fatalf("history = %T %+v, want messages", history, history)
|
||||
}
|
||||
msg, ok := msgs[0].(*tg.Message)
|
||||
if !ok || msg.Message != body || msg.Out != wantOut {
|
||||
t.Fatalf("latest message = %#v, want out=%v text=%q", msgs[0], wantOut, body)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatalf("send %q: %v\nstdout:\n%s\nstderr:\n%s", body, err, stdout.String(), stderr.String())
|
||||
}
|
||||
}
|
||||
readLatest := func(storage *session.StorageMemory, from tg.User, body string, wantOut bool) {
|
||||
t.Helper()
|
||||
client := newClient(storage)
|
||||
if err := client.Run(ctx, func(ctx context.Context) error {
|
||||
raw := tg.NewClient(client)
|
||||
history, err := raw.MessagesGetHistory(ctx, &tg.MessagesGetHistoryRequest{
|
||||
Peer: &tg.InputPeerUser{UserID: from.ID, AccessHash: from.AccessHash},
|
||||
Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msgs := messagesOf(history)
|
||||
if len(msgs) == 0 {
|
||||
t.Fatalf("history = %T %+v, want messages", history, history)
|
||||
}
|
||||
msg, ok := msgs[0].(*tg.Message)
|
||||
if !ok || msg.Message != body || msg.Out != wantOut {
|
||||
t.Fatalf("latest message = %#v, want out=%v text=%q", msgs[0], wantOut, body)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatalf("read latest %q: %v\nstdout:\n%s\nstderr:\n%s", body, err, stdout.String(), stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
suffix := time.Now().UnixNano() % 100000000
|
||||
storageA := &session.StorageMemory{}
|
||||
storageB := &session.StorageMemory{}
|
||||
userA := signUp(storageA, fmt.Sprintf("+1555%08d", suffix), "ExeAlice")
|
||||
userB := signUp(storageB, fmt.Sprintf("+1556%08d", suffix), "ExeBob")
|
||||
|
||||
sendAndAssertHistory(storageA, userB, "exe hello bob", time.Now().UnixNano(), true)
|
||||
readLatest(storageB, userA, "exe hello bob", false)
|
||||
sendAndAssertHistory(storageB, userA, "exe hi alice", time.Now().UnixNano()+1, true)
|
||||
readLatest(storageA, userB, "exe hi alice", false)
|
||||
}
|
||||
|
||||
type noSecretObfuscatedResolver struct {
|
||||
addr string
|
||||
dc int
|
||||
}
|
||||
|
||||
func (r noSecretObfuscatedResolver) Primary(ctx context.Context, _ int, _ dcs.List) (transport.Conn, error) {
|
||||
return r.connect(ctx)
|
||||
}
|
||||
|
||||
func (r noSecretObfuscatedResolver) MediaOnly(ctx context.Context, _ int, _ dcs.List) (transport.Conn, error) {
|
||||
return r.connect(ctx)
|
||||
}
|
||||
|
||||
func (r noSecretObfuscatedResolver) CDN(ctx context.Context, _ int, _ dcs.List) (transport.Conn, error) {
|
||||
return r.connect(ctx)
|
||||
}
|
||||
|
||||
func (r noSecretObfuscatedResolver) connect(ctx context.Context) (_ transport.Conn, rerr error) {
|
||||
var dialer net.Dialer
|
||||
conn, err := dialer.DialContext(ctx, "tcp", r.addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
_ = conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
obfsConn := obfuscator.Obfuscated2(tdcrypto.DefaultRand(), conn)
|
||||
if err := obfsConn.Handshake(codec.IntermediateClientStart, r.dc, mtproxy.Secret{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proto := transport.NewProtocol(func() transport.Codec {
|
||||
return codec.NoHeader{Codec: codec.Intermediate{}}
|
||||
})
|
||||
return proto.Handshake(obfsConn)
|
||||
}
|
||||
212
cmd/telesrv/main.go
Normal file
212
cmd/telesrv/main.go
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
// Command telesrv 是基于 gotd/td 的 Telegram-like server(第一兼容目标:Telegram Desktop)。
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotd/td/clock"
|
||||
"github.com/gotd/td/exchange"
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
"telesrv/internal/app/account"
|
||||
"telesrv/internal/app/auth"
|
||||
channelapp "telesrv/internal/app/channels"
|
||||
"telesrv/internal/app/contacts"
|
||||
"telesrv/internal/app/dialogs"
|
||||
filesapp "telesrv/internal/app/files"
|
||||
"telesrv/internal/app/help"
|
||||
"telesrv/internal/app/langpack"
|
||||
"telesrv/internal/app/maintenance"
|
||||
messageapp "telesrv/internal/app/messages"
|
||||
"telesrv/internal/app/updates"
|
||||
"telesrv/internal/app/users"
|
||||
"telesrv/internal/config"
|
||||
"telesrv/internal/mtprotoedge"
|
||||
"telesrv/internal/rpc"
|
||||
"telesrv/internal/store/postgres"
|
||||
"telesrv/internal/store/redisstore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "init logger:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() { _ = logger.Sync() }()
|
||||
|
||||
if err := run(logger); err != nil {
|
||||
logger.Error("telesrv 退出", zap.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(logger *zap.Logger) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
rsaKey, err := mtprotoedge.LoadOrGenerateRSAKey(cfg.RSAKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("server rsa key: %w", err)
|
||||
}
|
||||
fingerprint := exchange.PrivateKey{RSA: rsaKey}.Fingerprint()
|
||||
|
||||
_, portStr, err := net.SplitHostPort(cfg.ListenAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse listen addr %q: %w", cfg.ListenAddr, err)
|
||||
}
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse listen port %q: %w", portStr, err)
|
||||
}
|
||||
|
||||
// tg.Layer 来自 gotd/td v0.144.0(应为 225),需与目标 TDesktop 基线对齐。
|
||||
logger.Info("telesrv 启动",
|
||||
zap.String("listen", cfg.ListenAddr),
|
||||
zap.Int("dc", cfg.DC),
|
||||
zap.String("advertise", net.JoinHostPort(cfg.AdvertiseIP, portStr)),
|
||||
zap.Int("tl_layer", tg.Layer),
|
||||
zap.String("rsa_key", cfg.RSAKeyPath),
|
||||
zap.Int64("rsa_fingerprint", fingerprint),
|
||||
)
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
// 持久化依赖:先迁移 schema,再建立连接。auth_key 落 PostgreSQL、session 落 Redis。
|
||||
// 依赖由 deploy/docker-compose.yml 启动;连不上则启动失败(开发期须先 docker compose up)。
|
||||
if err := postgres.Migrate(cfg.PostgresDSN); err != nil {
|
||||
return fmt.Errorf("postgres migrate: %w", err)
|
||||
}
|
||||
pool, err := postgres.Open(ctx, cfg.PostgresDSN,
|
||||
postgres.WithMaxConns(cfg.PostgresMaxConns),
|
||||
postgres.WithMinConns(cfg.PostgresMinConns),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect postgres: %w", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
rdb, err := redisstore.Open(ctx, cfg.RedisAddr, cfg.RedisPassword, cfg.RedisDB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect redis: %w", err)
|
||||
}
|
||||
defer func() { _ = rdb.Close() }()
|
||||
logger.Info("持久化依赖就绪", zap.String("redis", cfg.RedisAddr))
|
||||
|
||||
ln, err := net.Listen("tcp", cfg.ListenAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen %q: %w", cfg.ListenAddr, err)
|
||||
}
|
||||
|
||||
authKeyStore := postgres.NewAuthKeyStore(pool)
|
||||
userStore := postgres.NewUserStore(pool)
|
||||
authzStore := postgres.NewAuthorizationStore(pool)
|
||||
updateStateStore := postgres.NewUpdateStateStore(pool)
|
||||
updateEventStore := postgres.NewUpdateEventStore(pool)
|
||||
dispatchOutboxStore := postgres.NewDispatchOutboxStore(pool, postgres.WithLeaseTimeout(cfg.OutboxLeaseTimeout))
|
||||
ptsAllocator := redisstore.NewPtsAllocator(rdb, updateEventStore)
|
||||
boxIDAllocator := redisstore.NewBoxIDAllocator(rdb, postgres.NewMessageBoxCounterSource(pool))
|
||||
channelIDAllocator := redisstore.NewChannelIDAllocator(rdb, postgres.NewChannelIDCounterSource(pool))
|
||||
channelPtsAllocator := redisstore.NewChannelPtsAllocator(rdb, postgres.NewChannelPtsCounterSource(pool))
|
||||
channelMessageIDAllocator := redisstore.NewChannelMessageIDAllocator(rdb, postgres.NewChannelMessageIDCounterSource(pool))
|
||||
contactStore := postgres.NewContactStore(pool)
|
||||
dialogStore := postgres.NewDialogStore(pool)
|
||||
messageStore := postgres.NewMessageStore(pool, postgres.WithMessageAllocators(boxIDAllocator, ptsAllocator))
|
||||
channelStore := postgres.NewChannelStore(pool, postgres.WithChannelAllocators(channelIDAllocator, channelPtsAllocator, channelMessageIDAllocator))
|
||||
mediaStore := postgres.NewMediaStore(pool)
|
||||
blobBackend, err := filesapp.NewLocalFS(cfg.BlobDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init blob backend: %w", err)
|
||||
}
|
||||
filesService := filesapp.NewService(mediaStore, blobBackend, cfg.DC)
|
||||
if stats, err := filesService.SeedMedia(ctx, cfg.StickerSeedDir, cfg.StickerSeedMaxSets); err != nil {
|
||||
return fmt.Errorf("seed media: %w", err)
|
||||
} else if !stats.Skipped {
|
||||
logger.Info("媒体种子导入完成",
|
||||
zap.String("dir", cfg.StickerSeedDir),
|
||||
zap.Int("reactions", stats.Reactions),
|
||||
zap.Int("sticker_sets", stats.StickerSets),
|
||||
zap.Int("documents", stats.Documents),
|
||||
zap.Int("blobs", stats.Blobs),
|
||||
)
|
||||
}
|
||||
if stats, err := filesService.WarmCaches(ctx); err != nil {
|
||||
logger.Warn("媒体资源缓存预热失败", zap.Error(err))
|
||||
} else if stats.StickerSets > 0 || stats.Documents > 0 || stats.Blobs > 0 {
|
||||
logger.Info("媒体资源缓存预热完成",
|
||||
zap.Int("sticker_sets", stats.StickerSets),
|
||||
zap.Int("documents", stats.Documents),
|
||||
zap.Int("blobs", stats.Blobs),
|
||||
)
|
||||
}
|
||||
langPackStore := postgres.NewLangPackStore(pool)
|
||||
passwordStore := postgres.NewPasswordStore(pool)
|
||||
helpStore := postgres.NewHelpStore(pool)
|
||||
tempAuthKeyStore := postgres.NewTempAuthKeyBindingStore(pool)
|
||||
sessionStore := redisstore.NewSessionStore(rdb, redisstore.DefaultSessionTTL)
|
||||
codeStore := redisstore.NewCodeStore(rdb)
|
||||
rateLimiter := redisstore.NewRateLimiter(rdb)
|
||||
activeSessions := mtprotoedge.NewSessionManager(logger.Named("mtprotoedge").Named("sessions"))
|
||||
go maintenance.NewRetentionWorker(dispatchOutboxStore, logger.Named("maintenance").Named("retention"),
|
||||
cfg.UpdateEventRetention,
|
||||
cfg.RetentionInterval,
|
||||
cfg.RetentionBatch,
|
||||
).Run(ctx)
|
||||
go rpc.NewOutboxDispatcher(updateEventStore, dispatchOutboxStore, activeSessions, logger.Named("rpc").Named("outbox"),
|
||||
rpc.WithOutboxWorkers(cfg.OutboxWorkers),
|
||||
rpc.WithOutboxBatch(cfg.OutboxBatch),
|
||||
rpc.WithOutboxInterval(cfg.OutboxInterval),
|
||||
rpc.WithOutboxPushTimeout(cfg.OutboundPushTimeout),
|
||||
).Run(ctx)
|
||||
langPackService := langpack.NewService(langPackStore)
|
||||
if seeded, err := langPackService.SeedDirectory(ctx, cfg.LangPackSeedDir); err != nil {
|
||||
return fmt.Errorf("seed langpack: %w", err)
|
||||
} else if seeded > 0 {
|
||||
logger.Info("语言包种子导入完成", zap.String("dir", cfg.LangPackSeedDir), zap.Int("strings", seeded))
|
||||
}
|
||||
|
||||
router := rpc.New(rpc.Config{
|
||||
DC: cfg.DC,
|
||||
IP: cfg.AdvertiseIP,
|
||||
Port: port,
|
||||
OutboundPushTimeout: cfg.OutboundPushTimeout,
|
||||
}, rpc.Deps{
|
||||
Auth: auth.NewService(userStore, authzStore, codeStore, authKeyStore, tempAuthKeyStore, cfg.DevAuthCode, auth.WithLoginMessages(messageStore, dialogStore)),
|
||||
Account: account.NewService(passwordStore, account.WithReactionSettings(passwordStore)),
|
||||
Help: help.NewService(helpStore, helpStore),
|
||||
Users: users.NewService(userStore, users.WithPhotoProvider(mediaStore)),
|
||||
Updates: updates.NewService(updateStateStore, updateEventStore, updates.WithPtsAllocator(ptsAllocator)),
|
||||
Contacts: contacts.NewService(contactStore, userStore),
|
||||
Dialogs: dialogs.NewService(dialogStore, channelStore),
|
||||
Messages: messageapp.NewService(messageStore, dialogStore),
|
||||
Channels: channelapp.NewService(channelStore),
|
||||
Files: filesService,
|
||||
LangPack: langPackService,
|
||||
Sessions: activeSessions,
|
||||
Limiter: rateLimiter,
|
||||
}, logger.Named("rpc"), clock.System)
|
||||
activeSessions.SetLifecycleObserver(router)
|
||||
|
||||
srv := mtprotoedge.New(mtprotoedge.Options{
|
||||
Logger: logger.Named("mtprotoedge"),
|
||||
DC: cfg.DC,
|
||||
RSAKey: rsaKey,
|
||||
RPC: router,
|
||||
AuthKeys: authKeyStore,
|
||||
Sessions: sessionStore,
|
||||
ActiveSessions: activeSessions,
|
||||
ObfuscatedTCP: true,
|
||||
})
|
||||
return srv.Serve(ctx, ln)
|
||||
}
|
||||
9104
data/langpack/tdesktop/tdesktop_en_v12000000.strings
Normal file
9104
data/langpack/tdesktop/tdesktop_en_v12000000.strings
Normal file
File diff suppressed because it is too large
Load diff
7130
data/langpack/tdesktop/tdesktop_zh-hans_v12000000.strings
Normal file
7130
data/langpack/tdesktop/tdesktop_zh-hans_v12000000.strings
Normal file
File diff suppressed because it is too large
Load diff
10
deploy/deploy.go
Normal file
10
deploy/deploy.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Package deploy 提供部署期资源(迁移脚本)的嵌入访问。
|
||||
package deploy
|
||||
|
||||
import "embed"
|
||||
|
||||
// Migrations 是嵌入的 golang-migrate 迁移脚本(deploy/migrations/*.sql)。
|
||||
// 供 store/postgres 的迁移 runner 在启动时执行,避免运行时依赖外部文件路径。
|
||||
//
|
||||
//go:embed migrations/*.sql
|
||||
var Migrations embed.FS
|
||||
48
deploy/docker-compose.yml
Normal file
48
deploy/docker-compose.yml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# telesrv 第一阶段三方依赖(开发用)。
|
||||
#
|
||||
# 启动: docker compose up -d
|
||||
# 状态: docker compose ps
|
||||
# 关停: docker compose down (保留数据卷)
|
||||
# 清空: docker compose down -v (连数据卷一起删,重置 schema/数据)
|
||||
#
|
||||
# server 端连接串见 internal/config/config.go 的默认值(localhost:5432 / localhost:6399)。
|
||||
name: telesrv
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
container_name: telesrv-postgres
|
||||
environment:
|
||||
POSTGRES_DB: telesrv
|
||||
POSTGRES_USER: telesrv
|
||||
POSTGRES_PASSWORD: telesrv
|
||||
TZ: UTC
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U telesrv -d telesrv"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: telesrv-redis
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
ports:
|
||||
- "6399:6379" # 宿主 6379 常被其他项目占用,telesrv 对外用 6399(容器内仍 6379)
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
redisdata:
|
||||
4
deploy/migrations/0001_init.down.sql
Normal file
4
deploy/migrations/0001_init.down.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
-- 0001_init 回滚:按外键依赖逆序删表。
|
||||
DROP TABLE IF EXISTS authorizations;
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP TABLE IF EXISTS auth_keys;
|
||||
48
deploy/migrations/0001_init.up.sql
Normal file
48
deploy/migrations/0001_init.up.sql
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
-- 0001_init: telesrv 第一阶段持久化 schema(auth_key / user / authorization)。
|
||||
--
|
||||
-- 协议产物(auth_keys)与业务产物(authorizations)分表,遵循协议/业务隔离边界。
|
||||
|
||||
-- MTProto auth key:密钥交换产物。server 重启后据此解密客户端加密包,避免客户端重建密钥。
|
||||
CREATE TABLE IF NOT EXISTS auth_keys (
|
||||
auth_key_id BIGINT PRIMARY KEY, -- SHA1(auth_key) 低 64 位,小端解释为 int64
|
||||
body BYTEA NOT NULL, -- 256 字节 auth key 原文
|
||||
server_salt BIGINT NOT NULL, -- 密钥交换产出的初始 server salt
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- 用户:第一阶段仅登录链路必须字段。access_hash 为任何 InputUser 校验所必须,不可省。
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
-- 普通用户 ID 从 2026-06-01 00:00:00 Asia/Shanghai 的 Unix 秒级时间戳开始。
|
||||
-- 777000 等系统兼容账号显式插入,低于该区间。
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1780243200) PRIMARY KEY,
|
||||
access_hash BIGINT NOT NULL,
|
||||
phone VARCHAR(32) NOT NULL,
|
||||
first_name VARCHAR(64) NOT NULL DEFAULT '',
|
||||
last_name VARCHAR(64) NOT NULL DEFAULT '',
|
||||
username VARCHAR(64) NOT NULL DEFAULT '',
|
||||
country_code VARCHAR(8) NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT users_phone_key UNIQUE (phone)
|
||||
);
|
||||
|
||||
-- 授权:auth_key ↔ user 绑定 + initConnection 带来的设备信息。
|
||||
-- 这是 auth.signIn 后绑定关系与 account.getAuthorizations 的权威来源;
|
||||
-- 也是 rpc.ClientInfo(device/app/layer)的持久化归宿。
|
||||
CREATE TABLE IF NOT EXISTS authorizations (
|
||||
auth_key_id BIGINT PRIMARY KEY REFERENCES auth_keys(auth_key_id) ON DELETE CASCADE,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
hash BIGINT NOT NULL DEFAULT 0, -- getAuthorizations 列表项 hash
|
||||
layer INT NOT NULL DEFAULT 0,
|
||||
device_model VARCHAR(128) NOT NULL DEFAULT '',
|
||||
platform VARCHAR(64) NOT NULL DEFAULT '',
|
||||
system_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
api_id INT NOT NULL DEFAULT 0,
|
||||
app_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
ip VARCHAR(64) NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
active_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- 按 user 反查授权(PushToUser / getAuthorizations)。
|
||||
CREATE INDEX IF NOT EXISTS authorizations_user_id_idx ON authorizations (user_id);
|
||||
5
deploy/migrations/0002_phase1_business.down.sql
Normal file
5
deploy/migrations/0002_phase1_business.down.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
DROP TABLE IF EXISTS lang_pack_strings;
|
||||
DROP TABLE IF EXISTS lang_packs;
|
||||
DROP TABLE IF EXISTS dialogs;
|
||||
DROP TABLE IF EXISTS contacts;
|
||||
DROP TABLE IF EXISTS update_states;
|
||||
73
deploy/migrations/0002_phase1_business.up.sql
Normal file
73
deploy/migrations/0002_phase1_business.up.sql
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
-- 0002_phase1_business: first-stage business persistence for startup RPCs.
|
||||
--
|
||||
-- 表结构按 telesrv domain/store 边界建模,不引入旧工程依赖。
|
||||
|
||||
CREATE TABLE IF NOT EXISTS update_states (
|
||||
auth_key_id BIGINT PRIMARY KEY REFERENCES auth_keys(auth_key_id) ON DELETE CASCADE,
|
||||
pts INT NOT NULL DEFAULT 0,
|
||||
qts INT NOT NULL DEFAULT 0,
|
||||
date INT NOT NULL DEFAULT 0,
|
||||
seq INT NOT NULL DEFAULT 0,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contacts (
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
contact_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
mutual BOOLEAN NOT NULL DEFAULT false,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (user_id, contact_user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS contacts_contact_user_id_idx ON contacts (contact_user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dialogs (
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
peer_type VARCHAR(16) NOT NULL,
|
||||
peer_id BIGINT NOT NULL,
|
||||
top_message_id INT NOT NULL DEFAULT 0,
|
||||
read_inbox_max_id INT NOT NULL DEFAULT 0,
|
||||
read_outbox_max_id INT NOT NULL DEFAULT 0,
|
||||
unread_count INT NOT NULL DEFAULT 0,
|
||||
unread_mentions_count INT NOT NULL DEFAULT 0,
|
||||
unread_reactions_count INT NOT NULL DEFAULT 0,
|
||||
pinned BOOLEAN NOT NULL DEFAULT false,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (user_id, peer_type, peer_id),
|
||||
CONSTRAINT dialogs_peer_type_check CHECK (peer_type IN ('user'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dialogs_user_updated_idx ON dialogs (user_id, updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS dialogs_user_pinned_idx ON dialogs (user_id, pinned) WHERE pinned;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lang_packs (
|
||||
lang_pack VARCHAR(32) NOT NULL,
|
||||
lang_code VARCHAR(64) NOT NULL,
|
||||
version INT NOT NULL,
|
||||
strings_count INT NOT NULL DEFAULT 0,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (lang_pack, lang_code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lang_pack_strings (
|
||||
lang_pack VARCHAR(32) NOT NULL,
|
||||
lang_code VARCHAR(64) NOT NULL,
|
||||
key VARCHAR(128) NOT NULL,
|
||||
version INT NOT NULL,
|
||||
pluralized BOOLEAN NOT NULL DEFAULT false,
|
||||
value TEXT NOT NULL DEFAULT '',
|
||||
zero_value TEXT NOT NULL DEFAULT '',
|
||||
one_value TEXT NOT NULL DEFAULT '',
|
||||
two_value TEXT NOT NULL DEFAULT '',
|
||||
few_value TEXT NOT NULL DEFAULT '',
|
||||
many_value TEXT NOT NULL DEFAULT '',
|
||||
other_value TEXT NOT NULL DEFAULT '',
|
||||
deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (lang_pack, lang_code, key),
|
||||
FOREIGN KEY (lang_pack, lang_code) REFERENCES lang_packs(lang_pack, lang_code) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS lang_pack_strings_pack_version_idx
|
||||
ON lang_pack_strings (lang_pack, lang_code, version);
|
||||
5
deploy/migrations/0003_startup_config_security.down.sql
Normal file
5
deploy/migrations/0003_startup_config_security.down.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
DROP TABLE IF EXISTS temp_auth_key_bindings;
|
||||
DROP TABLE IF EXISTS country_codes;
|
||||
DROP TABLE IF EXISTS countries;
|
||||
DROP TABLE IF EXISTS app_configs;
|
||||
DROP TABLE IF EXISTS account_passwords;
|
||||
64
deploy/migrations/0003_startup_config_security.up.sql
Normal file
64
deploy/migrations/0003_startup_config_security.up.sql
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
-- 0003_startup_config_security: data-backed startup config, countries and account security.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS account_passwords (
|
||||
user_id BIGINT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
has_recovery BOOLEAN NOT NULL DEFAULT false,
|
||||
has_secure_values BOOLEAN NOT NULL DEFAULT false,
|
||||
has_password BOOLEAN NOT NULL DEFAULT false,
|
||||
hint VARCHAR(256) NOT NULL DEFAULT '',
|
||||
email_unconfirmed_pattern VARCHAR(256) NOT NULL DEFAULT '',
|
||||
login_email_pattern VARCHAR(256) NOT NULL DEFAULT '',
|
||||
secure_random BYTEA NOT NULL DEFAULT decode('74656c657372762d746465736b746f702d6465762d7365637572652d72616e64', 'hex'),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app_configs (
|
||||
client VARCHAR(64) PRIMARY KEY,
|
||||
hash INT NOT NULL,
|
||||
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS countries (
|
||||
iso2 VARCHAR(2) PRIMARY KEY,
|
||||
default_name VARCHAR(128) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
hidden BOOLEAN NOT NULL DEFAULT false,
|
||||
order_index INT NOT NULL DEFAULT 0,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS country_codes (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
iso2 VARCHAR(2) NOT NULL REFERENCES countries(iso2) ON DELETE CASCADE,
|
||||
country_code VARCHAR(16) NOT NULL,
|
||||
prefixes TEXT[] NOT NULL DEFAULT '{}',
|
||||
patterns TEXT[] NOT NULL DEFAULT '{}',
|
||||
order_index INT NOT NULL DEFAULT 0,
|
||||
UNIQUE (iso2, country_code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS temp_auth_key_bindings (
|
||||
temp_auth_key_id BIGINT PRIMARY KEY REFERENCES auth_keys(auth_key_id) ON DELETE CASCADE,
|
||||
perm_auth_key_id BIGINT NOT NULL,
|
||||
nonce BIGINT NOT NULL,
|
||||
expires_at INT NOT NULL,
|
||||
encrypted_message BYTEA NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
INSERT INTO app_configs (client, hash, config_json)
|
||||
VALUES ('tdesktop', 1, '{}'::jsonb)
|
||||
ON CONFLICT (client) DO NOTHING;
|
||||
|
||||
INSERT INTO countries (iso2, default_name, name, hidden, order_index)
|
||||
VALUES
|
||||
('US', 'United States', '', false, 10),
|
||||
('CN', 'China', '', false, 20)
|
||||
ON CONFLICT (iso2) DO NOTHING;
|
||||
|
||||
INSERT INTO country_codes (iso2, country_code, prefixes, patterns, order_index)
|
||||
VALUES
|
||||
('US', '1', ARRAY['1'], '{}'::text[], 10),
|
||||
('CN', '86', ARRAY['86'], '{}'::text[], 20)
|
||||
ON CONFLICT (iso2, country_code) DO NOTHING;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE temp_auth_key_bindings
|
||||
DROP COLUMN IF EXISTS temp_session_id;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
-- 0004_temp_auth_key_binding_session: persist validated bind_auth_key_inner session id.
|
||||
|
||||
ALTER TABLE temp_auth_key_bindings
|
||||
ADD COLUMN IF NOT EXISTS temp_session_id BIGINT NOT NULL DEFAULT 0;
|
||||
11
deploy/migrations/0005_system_login_messages.down.sql
Normal file
11
deploy/migrations/0005_system_login_messages.down.sql
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
DROP INDEX IF EXISTS messages_owner_date_idx;
|
||||
DROP INDEX IF EXISTS messages_owner_dialog_idx;
|
||||
DROP TABLE IF EXISTS messages;
|
||||
|
||||
DROP INDEX IF EXISTS dialogs_user_top_message_idx;
|
||||
ALTER TABLE dialogs DROP COLUMN IF EXISTS top_message_date;
|
||||
|
||||
DELETE FROM users WHERE id = 777000;
|
||||
ALTER TABLE users
|
||||
DROP COLUMN IF EXISTS support,
|
||||
DROP COLUMN IF EXISTS verified;
|
||||
44
deploy/migrations/0005_system_login_messages.up.sql
Normal file
44
deploy/migrations/0005_system_login_messages.up.sql
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
-- 0005_system_login_messages: official system account and first message persistence.
|
||||
--
|
||||
-- 777000 官方账号与登录消息推送;表结构按 telesrv domain/store 边界建模。
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS verified BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS support BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
INSERT INTO users (id, access_hash, phone, first_name, last_name, username, country_code, verified, support)
|
||||
VALUES (777000, 6599886787491911851, '42777', 'Telegram', '', 'telegram', '', true, true)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
access_hash = EXCLUDED.access_hash,
|
||||
phone = EXCLUDED.phone,
|
||||
first_name = EXCLUDED.first_name,
|
||||
last_name = EXCLUDED.last_name,
|
||||
username = EXCLUDED.username,
|
||||
verified = EXCLUDED.verified,
|
||||
support = EXCLUDED.support,
|
||||
updated_at = now();
|
||||
|
||||
ALTER TABLE dialogs
|
||||
ADD COLUMN IF NOT EXISTS top_message_date INT NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dialogs_user_top_message_idx
|
||||
ON dialogs (user_id, pinned DESC, top_message_date DESC, top_message_id DESC, peer_id DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
owner_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
peer_type VARCHAR(16) NOT NULL,
|
||||
peer_id BIGINT NOT NULL,
|
||||
from_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
message_date INT NOT NULL,
|
||||
outgoing BOOLEAN NOT NULL DEFAULT false,
|
||||
body TEXT NOT NULL DEFAULT '',
|
||||
entities JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT messages_peer_type_check CHECK (peer_type IN ('user'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS messages_owner_dialog_idx
|
||||
ON messages (owner_user_id, peer_type, peer_id, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS messages_owner_date_idx
|
||||
ON messages (owner_user_id, message_date DESC, id DESC);
|
||||
2
deploy/migrations/0006_update_events.down.sql
Normal file
2
deploy/migrations/0006_update_events.down.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
DROP INDEX IF EXISTS update_events_auth_pts_idx;
|
||||
DROP TABLE IF EXISTS update_events;
|
||||
16
deploy/migrations/0006_update_events.up.sql
Normal file
16
deploy/migrations/0006_update_events.up.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
-- 0006_update_events: minimal auth-key update queue for getDifference补偿.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS update_events (
|
||||
auth_key_id BIGINT NOT NULL REFERENCES auth_keys(auth_key_id) ON DELETE CASCADE,
|
||||
pts INT NOT NULL,
|
||||
pts_count INT NOT NULL DEFAULT 1,
|
||||
date INT NOT NULL,
|
||||
event_type VARCHAR(32) NOT NULL,
|
||||
message_id INT REFERENCES messages(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (auth_key_id, pts),
|
||||
CONSTRAINT update_events_type_check CHECK (event_type IN ('new_message'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS update_events_auth_pts_idx
|
||||
ON update_events (auth_key_id, pts);
|
||||
15
deploy/migrations/0007_read_history_events.down.sql
Normal file
15
deploy/migrations/0007_read_history_events.down.sql
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
ALTER TABLE update_events
|
||||
DROP CONSTRAINT IF EXISTS update_events_peer_type_check;
|
||||
|
||||
ALTER TABLE update_events
|
||||
DROP CONSTRAINT IF EXISTS update_events_type_check;
|
||||
|
||||
ALTER TABLE update_events
|
||||
ADD CONSTRAINT update_events_type_check
|
||||
CHECK (event_type IN ('new_message'));
|
||||
|
||||
ALTER TABLE update_events
|
||||
DROP COLUMN IF EXISTS still_unread_count,
|
||||
DROP COLUMN IF EXISTS max_id,
|
||||
DROP COLUMN IF EXISTS peer_id,
|
||||
DROP COLUMN IF EXISTS peer_type;
|
||||
23
deploy/migrations/0007_read_history_events.up.sql
Normal file
23
deploy/migrations/0007_read_history_events.up.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
-- 0007_read_history_events: persist readHistory update events for getDifference.
|
||||
--
|
||||
-- updateReadHistoryInbox:真正发生已读推进时递增 pts,并给其它 session / getDifference 留可补偿事件。
|
||||
|
||||
ALTER TABLE update_events
|
||||
ADD COLUMN IF NOT EXISTS peer_type VARCHAR(16),
|
||||
ADD COLUMN IF NOT EXISTS peer_id BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS max_id INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS still_unread_count INT NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE update_events
|
||||
DROP CONSTRAINT IF EXISTS update_events_type_check;
|
||||
|
||||
ALTER TABLE update_events
|
||||
ADD CONSTRAINT update_events_type_check
|
||||
CHECK (event_type IN ('new_message', 'read_history_inbox'));
|
||||
|
||||
ALTER TABLE update_events
|
||||
DROP CONSTRAINT IF EXISTS update_events_peer_type_check;
|
||||
|
||||
ALTER TABLE update_events
|
||||
ADD CONSTRAINT update_events_peer_type_check
|
||||
CHECK (peer_type IS NULL OR peer_type IN ('user'));
|
||||
8
deploy/migrations/0008_user_id_sequence_base.down.sql
Normal file
8
deploy/migrations/0008_user_id_sequence_base.down.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- Revert generated ordinary user ids to the original first-phase base when possible.
|
||||
-- Existing users keep their ids; the next generated id is never moved below MAX(id)+1.
|
||||
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('users', 'id'),
|
||||
GREATEST((SELECT COALESCE(MAX(id), 0) FROM users), 999999999),
|
||||
true
|
||||
);
|
||||
10
deploy/migrations/0008_user_id_sequence_base.up.sql
Normal file
10
deploy/migrations/0008_user_id_sequence_base.up.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-- 0008_user_id_sequence_base: move ordinary user ids to the agreed timestamp range.
|
||||
--
|
||||
-- Base: 2026-06-01 00:00:00 Asia/Shanghai => Unix seconds 1780243200.
|
||||
-- Existing users keep their ids; the next generated id is at least this base.
|
||||
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('users', 'id'),
|
||||
GREATEST((SELECT COALESCE(MAX(id), 0) FROM users), 1780243199),
|
||||
true
|
||||
);
|
||||
13
deploy/migrations/0009_private_message_pipeline.down.sql
Normal file
13
deploy/migrations/0009_private_message_pipeline.down.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
-- 0009 rollback: remove second-stage private message pipeline tables.
|
||||
DROP TABLE IF EXISTS dispatch_outbox;
|
||||
DROP TABLE IF EXISTS user_update_events;
|
||||
DROP TABLE IF EXISTS dialogs;
|
||||
DROP TABLE IF EXISTS message_boxes;
|
||||
DROP TABLE IF EXISTS private_messages;
|
||||
|
||||
ALTER TABLE IF EXISTS dialogs_legacy RENAME TO dialogs;
|
||||
ALTER TABLE IF EXISTS messages_legacy RENAME TO messages;
|
||||
|
||||
ALTER TABLE update_states DROP CONSTRAINT IF EXISTS update_states_pkey;
|
||||
ALTER TABLE update_states DROP COLUMN IF EXISTS user_id;
|
||||
ALTER TABLE update_states ADD PRIMARY KEY (auth_key_id);
|
||||
239
deploy/migrations/0009_private_message_pipeline.up.sql
Normal file
239
deploy/migrations/0009_private_message_pipeline.up.sql
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
-- 0009_private_message_pipeline: second-stage private text message storage.
|
||||
--
|
||||
-- Large tables are partitioned from the first version of the message module:
|
||||
-- message_boxes/dialogs/user_update_events/dispatch_outbox by owner/target user,
|
||||
-- private_messages by sender user for random_id idempotency locality.
|
||||
|
||||
ALTER TABLE IF EXISTS messages RENAME TO messages_legacy;
|
||||
ALTER TABLE IF EXISTS dialogs RENAME TO dialogs_legacy;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS private_messages (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
|
||||
sender_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
recipient_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
random_id BIGINT NOT NULL DEFAULT 0,
|
||||
message_date INT NOT NULL,
|
||||
body TEXT NOT NULL DEFAULT '',
|
||||
entities JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (sender_user_id, id),
|
||||
CONSTRAINT private_messages_nonempty_body CHECK (body <> '')
|
||||
) PARTITION BY HASH (sender_user_id);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS private_messages_sender_random_idx
|
||||
ON private_messages (sender_user_id, random_id)
|
||||
WHERE random_id <> 0;
|
||||
CREATE INDEX IF NOT EXISTS private_messages_recipient_date_idx
|
||||
ON private_messages (recipient_user_id, message_date DESC, id DESC);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS private_messages_p%s PARTITION OF private_messages FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS message_boxes (
|
||||
owner_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
box_id INT NOT NULL,
|
||||
private_message_id BIGINT NOT NULL,
|
||||
message_sender_id BIGINT NOT NULL,
|
||||
peer_type VARCHAR(16) NOT NULL,
|
||||
peer_id BIGINT NOT NULL,
|
||||
from_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
message_date INT NOT NULL,
|
||||
outgoing BOOLEAN NOT NULL DEFAULT false,
|
||||
body TEXT NOT NULL DEFAULT '',
|
||||
entities JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
pts INT NOT NULL DEFAULT 0,
|
||||
deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (owner_user_id, box_id),
|
||||
UNIQUE (owner_user_id, private_message_id),
|
||||
CONSTRAINT message_boxes_peer_type_check CHECK (peer_type IN ('user')),
|
||||
FOREIGN KEY (message_sender_id, private_message_id)
|
||||
REFERENCES private_messages(sender_user_id, id) ON DELETE CASCADE
|
||||
) PARTITION BY HASH (owner_user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS message_boxes_dialog_seek_idx
|
||||
ON message_boxes (owner_user_id, peer_type, peer_id, box_id DESC)
|
||||
WHERE NOT deleted;
|
||||
CREATE INDEX IF NOT EXISTS message_boxes_owner_date_idx
|
||||
ON message_boxes (owner_user_id, message_date DESC, box_id DESC)
|
||||
WHERE NOT deleted;
|
||||
CREATE INDEX IF NOT EXISTS message_boxes_private_lookup_idx
|
||||
ON message_boxes (private_message_id, owner_user_id);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS message_boxes_p%s PARTITION OF message_boxes FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dialogs (
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
peer_type VARCHAR(16) NOT NULL,
|
||||
peer_id BIGINT NOT NULL,
|
||||
top_message_id INT NOT NULL DEFAULT 0,
|
||||
top_message_date INT NOT NULL DEFAULT 0,
|
||||
read_inbox_max_id INT NOT NULL DEFAULT 0,
|
||||
read_outbox_max_id INT NOT NULL DEFAULT 0,
|
||||
unread_count INT NOT NULL DEFAULT 0,
|
||||
unread_mentions_count INT NOT NULL DEFAULT 0,
|
||||
unread_reactions_count INT NOT NULL DEFAULT 0,
|
||||
pinned BOOLEAN NOT NULL DEFAULT false,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (user_id, peer_type, peer_id),
|
||||
CONSTRAINT dialogs_peer_type_check CHECK (peer_type IN ('user'))
|
||||
) PARTITION BY HASH (user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dialogs_user_top_message_idx
|
||||
ON dialogs (user_id, pinned DESC, top_message_date DESC, top_message_id DESC, peer_id DESC);
|
||||
CREATE INDEX IF NOT EXISTS dialogs_user_pinned_idx
|
||||
ON dialogs (user_id, pinned) WHERE pinned;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS dialogs_p%s PARTITION OF dialogs FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_update_events (
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
pts INT NOT NULL,
|
||||
pts_count INT NOT NULL DEFAULT 1,
|
||||
date INT NOT NULL,
|
||||
event_type VARCHAR(32) NOT NULL,
|
||||
message_box_id INT,
|
||||
peer_type VARCHAR(16),
|
||||
peer_id BIGINT,
|
||||
max_id INT NOT NULL DEFAULT 0,
|
||||
still_unread_count INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (user_id, pts),
|
||||
CONSTRAINT user_update_events_type_check CHECK (event_type IN ('new_message', 'read_history_inbox', 'noop')),
|
||||
CONSTRAINT user_update_events_peer_type_check CHECK (peer_type IS NULL OR peer_type IN ('user')),
|
||||
FOREIGN KEY (user_id, message_box_id) REFERENCES message_boxes(owner_user_id, box_id) ON DELETE CASCADE
|
||||
) PARTITION BY HASH (user_id);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS user_update_events_p%s PARTITION OF user_update_events FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dispatch_outbox (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
|
||||
target_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
pts INT NOT NULL,
|
||||
event_type VARCHAR(32) NOT NULL,
|
||||
exclude_session_id BIGINT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
attempts INT NOT NULL DEFAULT 0,
|
||||
next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_error TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (target_user_id, id),
|
||||
CONSTRAINT dispatch_outbox_status_check CHECK (status IN ('pending', 'dispatching', 'delivered', 'failed')),
|
||||
FOREIGN KEY (target_user_id, pts) REFERENCES user_update_events(user_id, pts) ON DELETE CASCADE
|
||||
) PARTITION BY HASH (target_user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dispatch_outbox_pending_idx
|
||||
ON dispatch_outbox (status, next_attempt_at, target_user_id, id)
|
||||
WHERE status = 'pending';
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS dispatch_outbox_p%s PARTITION OF dispatch_outbox FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE update_states
|
||||
ADD COLUMN IF NOT EXISTS user_id BIGINT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE update_states DROP CONSTRAINT IF EXISTS update_states_pkey;
|
||||
ALTER TABLE update_states ADD PRIMARY KEY (auth_key_id, user_id);
|
||||
CREATE INDEX IF NOT EXISTS update_states_user_id_idx ON update_states (user_id);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
r record;
|
||||
private_id bigint;
|
||||
BEGIN
|
||||
IF to_regclass('messages_legacy') IS NULL THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
FOR r IN
|
||||
SELECT id, owner_user_id, peer_type, peer_id, from_user_id, message_date, outgoing, body, entities
|
||||
FROM messages_legacy
|
||||
ORDER BY owner_user_id, id
|
||||
LOOP
|
||||
INSERT INTO private_messages (
|
||||
sender_user_id, recipient_user_id, random_id, message_date, body, entities
|
||||
) VALUES (
|
||||
r.from_user_id,
|
||||
r.owner_user_id,
|
||||
0,
|
||||
r.message_date,
|
||||
r.body,
|
||||
r.entities
|
||||
)
|
||||
RETURNING id INTO private_id;
|
||||
|
||||
INSERT INTO message_boxes (
|
||||
owner_user_id, box_id, private_message_id, message_sender_id, peer_type, peer_id,
|
||||
from_user_id, message_date, outgoing, body, entities
|
||||
) VALUES (
|
||||
r.owner_user_id, r.id, private_id, r.from_user_id, r.peer_type, r.peer_id,
|
||||
r.from_user_id, r.message_date, r.outgoing, r.body, r.entities
|
||||
)
|
||||
ON CONFLICT (owner_user_id, box_id) DO NOTHING;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
INSERT INTO dialogs (
|
||||
user_id, peer_type, peer_id, top_message_id, top_message_date,
|
||||
read_inbox_max_id, read_outbox_max_id, unread_count,
|
||||
unread_mentions_count, unread_reactions_count, pinned, updated_at
|
||||
)
|
||||
SELECT
|
||||
user_id, peer_type, peer_id, top_message_id, top_message_date,
|
||||
read_inbox_max_id, read_outbox_max_id, unread_count,
|
||||
unread_mentions_count, unread_reactions_count, pinned, updated_at
|
||||
FROM dialogs_legacy
|
||||
ON CONFLICT (user_id, peer_type, peer_id) DO NOTHING;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
DROP INDEX IF EXISTS dispatch_outbox_dispatching_stale_idx;
|
||||
DROP INDEX IF EXISTS message_boxes_dialog_date_seek_idx;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
-- 0010_message_performance_indexes: indexes for second-stage message seek paths.
|
||||
|
||||
CREATE INDEX IF NOT EXISTS message_boxes_dialog_date_seek_idx
|
||||
ON message_boxes (owner_user_id, peer_type, peer_id, message_date DESC, box_id DESC)
|
||||
WHERE NOT deleted;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dispatch_outbox_dispatching_stale_idx
|
||||
ON dispatch_outbox (status, updated_at, target_user_id, id)
|
||||
WHERE status = 'dispatching';
|
||||
6
deploy/migrations/0011_drop_dead_tables.down.sql
Normal file
6
deploy/migrations/0011_drop_dead_tables.down.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-- 0011 down (no-op): 本迁移清除的是已被取代、运行时零引用的死表
|
||||
-- (update_events / messages_legacy / dialogs_legacy)。
|
||||
--
|
||||
-- 全新项目明确不保留回退路径,故不在此重建这些表;历史结构可查 0002 / 0005 / 0006 / 0007 迁移脚本(保留未删)。
|
||||
-- golang-migrate 执行本文件即把版本回退到 0010,不恢复任何死表或其数据。
|
||||
SELECT 1;
|
||||
13
deploy/migrations/0011_drop_dead_tables.up.sql
Normal file
13
deploy/migrations/0011_drop_dead_tables.up.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
-- 0011_drop_dead_tables: 清除已被取代、运行时零引用的遗留表(全新项目,不保留回退)。
|
||||
--
|
||||
-- - update_events : 一阶段 auth_key 维度 update 队列(0006 建 / 0007 扩展),二阶段被
|
||||
-- user_update_events 取代;无任何 query / Go 引用,仅在 sqlc 留下孤儿 model。
|
||||
-- - messages_legacy: 0009 由旧 messages(0005)重命名保留的迁移残骸,数据已迁入 private_messages + message_boxes。
|
||||
-- - dialogs_legacy : 0009 由旧 dialogs(0002)重命名保留的迁移残骸,数据已迁入新 dialogs。
|
||||
--
|
||||
-- 顺序要求:update_events.message_id 外键指向 messages_legacy(原 messages),故先删 update_events。
|
||||
-- 删除后需重跑 `sqlc generate`,models.go 中 UpdateEvent / MessagesLegacy / DialogsLegacy 孤儿 model 会自动消失。
|
||||
|
||||
DROP TABLE IF EXISTS update_events;
|
||||
DROP TABLE IF EXISTS messages_legacy;
|
||||
DROP TABLE IF EXISTS dialogs_legacy;
|
||||
5
deploy/migrations/0012_outbox_delete_on_deliver.down.sql
Normal file
5
deploy/migrations/0012_outbox_delete_on_deliver.down.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-- 0012 down: 恢复宽 status CHECK(含 'delivered')以保持迁移链完整。
|
||||
-- 注意:方案 A 已删除的 delivered 行不可恢复;query 层(DELETE)回退需手动改回 UPDATE,本 down 不涉及代码。
|
||||
ALTER TABLE dispatch_outbox DROP CONSTRAINT IF EXISTS dispatch_outbox_status_check;
|
||||
ALTER TABLE dispatch_outbox ADD CONSTRAINT dispatch_outbox_status_check
|
||||
CHECK (status IN ('pending', 'dispatching', 'delivered', 'failed'));
|
||||
12
deploy/migrations/0012_outbox_delete_on_deliver.up.sql
Normal file
12
deploy/migrations/0012_outbox_delete_on_deliver.up.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
-- 0012_outbox_delete_on_deliver: outbox 投递成功改为直接 DELETE(方案 A),杜绝 delivered 行无限堆积。
|
||||
--
|
||||
-- 配合 query 改动(MarkDispatchDelivered / MarkDispatchDeliveredBatch 由 UPDATE status='delivered' 改为 DELETE):
|
||||
-- 1) 清理改造前堆积的存量 delivered 行;
|
||||
-- 2) 收紧 status CHECK,移除不再使用的 'delivered'(状态机只剩 pending / dispatching / failed)。
|
||||
-- dispatch_outbox 为 HASH 分区表,父表 DELETE / ALTER CONSTRAINT 自动作用于全部分区。
|
||||
|
||||
DELETE FROM dispatch_outbox WHERE status = 'delivered';
|
||||
|
||||
ALTER TABLE dispatch_outbox DROP CONSTRAINT IF EXISTS dispatch_outbox_status_check;
|
||||
ALTER TABLE dispatch_outbox ADD CONSTRAINT dispatch_outbox_status_check
|
||||
CHECK (status IN ('pending', 'dispatching', 'failed'));
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
DROP INDEX IF EXISTS dialogs_user_pinned_order_idx;
|
||||
|
||||
ALTER TABLE dialogs
|
||||
DROP COLUMN IF EXISTS hidden_peer_settings_bar,
|
||||
DROP COLUMN IF EXISTS unread_mark,
|
||||
DROP COLUMN IF EXISTS pinned_order;
|
||||
|
||||
DROP INDEX IF EXISTS contacts_user_name_idx;
|
||||
|
||||
ALTER TABLE contacts
|
||||
DROP COLUMN IF EXISTS stories_hidden,
|
||||
DROP COLUMN IF EXISTS close_friend,
|
||||
DROP COLUMN IF EXISTS note_entities,
|
||||
DROP COLUMN IF EXISTS note,
|
||||
DROP COLUMN IF EXISTS contact_last_name,
|
||||
DROP COLUMN IF EXISTS contact_first_name,
|
||||
DROP COLUMN IF EXISTS contact_phone;
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
-- 0013_contact_profiles_and_dialog_pins: owner-scoped contact profile fields and pin order.
|
||||
--
|
||||
-- contact_* columns are deliberately scoped to (user_id, contact_user_id): they model the
|
||||
-- current account's saved name/phone/note for a peer and must not mutate users global data.
|
||||
|
||||
ALTER TABLE contacts
|
||||
ADD COLUMN IF NOT EXISTS contact_phone VARCHAR(32) NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS contact_first_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS contact_last_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS note TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS note_entities JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS close_friend BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS stories_hidden BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS contacts_user_name_idx
|
||||
ON contacts (user_id, contact_first_name, contact_last_name, contact_user_id);
|
||||
|
||||
ALTER TABLE dialogs
|
||||
ADD COLUMN IF NOT EXISTS pinned_order INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS unread_mark BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS hidden_peer_settings_bar BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dialogs_user_pinned_order_idx
|
||||
ON dialogs (user_id, pinned, pinned_order, top_message_date DESC, top_message_id DESC, peer_id DESC)
|
||||
WHERE pinned;
|
||||
10
deploy/migrations/0014_settings_update_events.down.sql
Normal file
10
deploy/migrations/0014_settings_update_events.down.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
ALTER TABLE user_update_events
|
||||
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD CONSTRAINT user_update_events_type_check CHECK (
|
||||
event_type IN ('new_message', 'read_history_inbox', 'noop')
|
||||
);
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP COLUMN IF EXISTS event_bool;
|
||||
25
deploy/migrations/0014_settings_update_events.up.sql
Normal file
25
deploy/migrations/0014_settings_update_events.up.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
-- 0014_settings_update_events: durable updates for contacts/dialog settings.
|
||||
--
|
||||
-- Online push is not enough: offline sessions must recover contact resets,
|
||||
-- dialog pin order changes, manual unread marks, and peer settings changes
|
||||
-- through updates.getDifference.
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD COLUMN IF NOT EXISTS event_bool BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD CONSTRAINT user_update_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'new_message',
|
||||
'read_history_inbox',
|
||||
'contacts_reset',
|
||||
'dialog_pinned',
|
||||
'pinned_dialogs',
|
||||
'dialog_unread_mark',
|
||||
'peer_settings',
|
||||
'noop'
|
||||
)
|
||||
);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
ALTER TABLE dispatch_outbox
|
||||
DROP COLUMN IF EXISTS exclude_auth_key_id;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP COLUMN IF EXISTS peer_settings;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP COLUMN IF EXISTS event_peers;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
-- 0015_update_event_payloads_and_outbox_auth: keep setting-update payloads durable.
|
||||
--
|
||||
-- event_peers carries ordered dialog peers for updatePinnedDialogs.order.
|
||||
-- peer_settings carries updatePeerSettings flags.
|
||||
-- exclude_auth_key_id makes outbox exclusion precise for same session_id across auth keys.
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD COLUMN IF NOT EXISTS event_peers JSONB NOT NULL DEFAULT '[]'::jsonb;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD COLUMN IF NOT EXISTS peer_settings JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
||||
ALTER TABLE dispatch_outbox
|
||||
ADD COLUMN IF NOT EXISTS exclude_auth_key_id BIGINT NOT NULL DEFAULT 0;
|
||||
21
deploy/migrations/0016_delete_message_updates.down.sql
Normal file
21
deploy/migrations/0016_delete_message_updates.down.sql
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
DROP INDEX IF EXISTS message_boxes_private_sender_live_idx;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD CONSTRAINT user_update_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'new_message',
|
||||
'read_history_inbox',
|
||||
'contacts_reset',
|
||||
'dialog_pinned',
|
||||
'pinned_dialogs',
|
||||
'dialog_unread_mark',
|
||||
'peer_settings',
|
||||
'noop'
|
||||
)
|
||||
);
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP COLUMN IF EXISTS message_ids;
|
||||
29
deploy/migrations/0016_delete_message_updates.up.sql
Normal file
29
deploy/migrations/0016_delete_message_updates.up.sql
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
-- 0016_delete_message_updates: durable owner-view delete message updates.
|
||||
--
|
||||
-- message_ids carries updateDeleteMessages.messages for offline getDifference
|
||||
-- and reliable online outbox delivery.
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD COLUMN IF NOT EXISTS message_ids JSONB NOT NULL DEFAULT '[]'::jsonb;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD CONSTRAINT user_update_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'new_message',
|
||||
'read_history_inbox',
|
||||
'contacts_reset',
|
||||
'dialog_pinned',
|
||||
'pinned_dialogs',
|
||||
'dialog_unread_mark',
|
||||
'peer_settings',
|
||||
'delete_messages',
|
||||
'noop'
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS message_boxes_private_sender_live_idx
|
||||
ON message_boxes (message_sender_id, private_message_id)
|
||||
WHERE NOT deleted;
|
||||
46
deploy/migrations/0017_dialog_folders.down.sql
Normal file
46
deploy/migrations/0017_dialog_folders.down.sql
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
-- 0017_dialog_folders rollback.
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD CONSTRAINT user_update_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'new_message',
|
||||
'read_history_inbox',
|
||||
'contacts_reset',
|
||||
'dialog_pinned',
|
||||
'pinned_dialogs',
|
||||
'dialog_unread_mark',
|
||||
'peer_settings',
|
||||
'delete_messages',
|
||||
'noop'
|
||||
)
|
||||
);
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP COLUMN IF EXISTS tags_enabled;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP COLUMN IF EXISTS filter_id;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP COLUMN IF EXISTS folder_peers;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP COLUMN IF EXISTS filter_order;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP COLUMN IF EXISTS dialog_filter;
|
||||
|
||||
DROP TABLE IF EXISTS dialog_filter_settings CASCADE;
|
||||
|
||||
DROP TABLE IF EXISTS dialog_filters CASCADE;
|
||||
|
||||
DROP INDEX IF EXISTS dialogs_user_folder_top_message_idx;
|
||||
|
||||
ALTER TABLE dialogs
|
||||
DROP CONSTRAINT IF EXISTS dialogs_folder_id_check;
|
||||
|
||||
ALTER TABLE dialogs
|
||||
DROP COLUMN IF EXISTS folder_id;
|
||||
91
deploy/migrations/0017_dialog_folders.up.sql
Normal file
91
deploy/migrations/0017_dialog_folders.up.sql
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
-- 0017_dialog_folders: archive folder, custom dialog filters, and durable folder updates.
|
||||
|
||||
ALTER TABLE dialogs
|
||||
ADD COLUMN IF NOT EXISTS folder_id INT NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE dialogs
|
||||
DROP CONSTRAINT IF EXISTS dialogs_folder_id_check;
|
||||
|
||||
ALTER TABLE dialogs
|
||||
ADD CONSTRAINT dialogs_folder_id_check CHECK (folder_id >= 0);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dialogs_user_folder_top_message_idx
|
||||
ON dialogs (user_id, folder_id, pinned DESC, top_message_date DESC, top_message_id DESC, peer_id DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dialog_filters (
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
filter_id INT NOT NULL,
|
||||
is_chatlist BOOLEAN NOT NULL DEFAULT false,
|
||||
filter JSONB NOT NULL,
|
||||
order_value INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (user_id, filter_id),
|
||||
CONSTRAINT dialog_filters_id_check CHECK (filter_id >= 2),
|
||||
CONSTRAINT dialog_filters_filter_object_check CHECK (jsonb_typeof(filter) = 'object')
|
||||
) PARTITION BY HASH (user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS dialog_filters_user_order_idx
|
||||
ON dialog_filters (user_id, order_value, filter_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dialog_filter_settings (
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
tags_enabled BOOLEAN NOT NULL DEFAULT false,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (user_id)
|
||||
) PARTITION BY HASH (user_id);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS dialog_filters_p%s PARTITION OF dialog_filters FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS dialog_filter_settings_p%s PARTITION OF dialog_filter_settings FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD COLUMN IF NOT EXISTS dialog_filter JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD COLUMN IF NOT EXISTS filter_order JSONB NOT NULL DEFAULT '[]'::jsonb;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD COLUMN IF NOT EXISTS folder_peers JSONB NOT NULL DEFAULT '[]'::jsonb;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD COLUMN IF NOT EXISTS filter_id INT NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD COLUMN IF NOT EXISTS tags_enabled BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD CONSTRAINT user_update_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'new_message',
|
||||
'read_history_inbox',
|
||||
'contacts_reset',
|
||||
'dialog_pinned',
|
||||
'pinned_dialogs',
|
||||
'dialog_unread_mark',
|
||||
'peer_settings',
|
||||
'delete_messages',
|
||||
'dialog_filter',
|
||||
'dialog_filter_order',
|
||||
'dialog_filters',
|
||||
'folder_peers',
|
||||
'noop'
|
||||
)
|
||||
);
|
||||
5
deploy/migrations/0018_user_search_indexes.down.sql
Normal file
5
deploy/migrations/0018_user_search_indexes.down.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
DROP INDEX IF EXISTS message_boxes_body_trgm_idx;
|
||||
DROP INDEX IF EXISTS contacts_user_saved_name_trgm_idx;
|
||||
DROP INDEX IF EXISTS users_name_lower_trgm_idx;
|
||||
DROP INDEX IF EXISTS users_username_lower_trgm_idx;
|
||||
DROP INDEX IF EXISTS users_phone_prefix_idx;
|
||||
19
deploy/migrations/0018_user_search_indexes.up.sql
Normal file
19
deploy/migrations/0018_user_search_indexes.up.sql
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
-- 0018_user_search_indexes: keep TDesktop global user search bounded as users grow.
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS users_phone_prefix_idx
|
||||
ON users (phone text_pattern_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS users_username_lower_trgm_idx
|
||||
ON users USING gin (lower(username) gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS users_name_lower_trgm_idx
|
||||
ON users USING gin (lower(trim(first_name || ' ' || last_name)) gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS contacts_user_saved_name_trgm_idx
|
||||
ON contacts USING gin (lower(trim(contact_first_name || ' ' || contact_last_name)) gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS message_boxes_body_trgm_idx
|
||||
ON message_boxes USING gin (body gin_trgm_ops)
|
||||
WHERE NOT deleted AND body <> '';
|
||||
1
deploy/migrations/0019_usernames.down.sql
Normal file
1
deploy/migrations/0019_usernames.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
DROP INDEX IF EXISTS users_username_lower_unique_idx;
|
||||
5
deploy/migrations/0019_usernames.up.sql
Normal file
5
deploy/migrations/0019_usernames.up.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-- 0019_usernames: primary username lifecycle.
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS users_username_lower_unique_idx
|
||||
ON users (lower(username))
|
||||
WHERE username <> '';
|
||||
34
deploy/migrations/0020_profile_message_state.down.sql
Normal file
34
deploy/migrations/0020_profile_message_state.down.sql
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
ALTER TABLE user_update_events
|
||||
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD CONSTRAINT user_update_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'new_message',
|
||||
'read_history_inbox',
|
||||
'contacts_reset',
|
||||
'dialog_pinned',
|
||||
'pinned_dialogs',
|
||||
'dialog_unread_mark',
|
||||
'peer_settings',
|
||||
'delete_messages',
|
||||
'dialog_filter',
|
||||
'dialog_filter_order',
|
||||
'dialog_filters',
|
||||
'folder_peers',
|
||||
'noop'
|
||||
)
|
||||
);
|
||||
|
||||
DROP INDEX IF EXISTS message_boxes_private_sender_owner_idx;
|
||||
DROP INDEX IF EXISTS user_update_events_read_outbox_idx;
|
||||
DROP INDEX IF EXISTS message_boxes_read_receipt_idx;
|
||||
|
||||
ALTER TABLE message_boxes
|
||||
DROP COLUMN IF EXISTS edit_date;
|
||||
|
||||
ALTER TABLE private_messages
|
||||
DROP COLUMN IF EXISTS edit_date;
|
||||
|
||||
ALTER TABLE users
|
||||
DROP COLUMN IF EXISTS about;
|
||||
46
deploy/migrations/0020_profile_message_state.up.sql
Normal file
46
deploy/migrations/0020_profile_message_state.up.sql
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
-- 0020_profile_message_state: profile about, message edits, and read outbox updates.
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS about VARCHAR(255) NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE private_messages
|
||||
ADD COLUMN IF NOT EXISTS edit_date INT NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE message_boxes
|
||||
ADD COLUMN IF NOT EXISTS edit_date INT NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS message_boxes_read_receipt_idx
|
||||
ON message_boxes (owner_user_id, peer_type, peer_id, box_id DESC)
|
||||
WHERE NOT deleted AND NOT outgoing;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS message_boxes_private_sender_owner_idx
|
||||
ON message_boxes (message_sender_id, private_message_id, owner_user_id)
|
||||
WHERE NOT deleted;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS user_update_events_read_outbox_idx
|
||||
ON user_update_events (user_id, peer_type, peer_id, max_id, date)
|
||||
WHERE event_type = 'read_history_outbox';
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD CONSTRAINT user_update_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'new_message',
|
||||
'read_history_inbox',
|
||||
'read_history_outbox',
|
||||
'edit_message',
|
||||
'contacts_reset',
|
||||
'dialog_pinned',
|
||||
'pinned_dialogs',
|
||||
'dialog_unread_mark',
|
||||
'peer_settings',
|
||||
'delete_messages',
|
||||
'dialog_filter',
|
||||
'dialog_filter_order',
|
||||
'dialog_filters',
|
||||
'folder_peers',
|
||||
'noop'
|
||||
)
|
||||
);
|
||||
31
deploy/migrations/0021_message_reply_forward.down.sql
Normal file
31
deploy/migrations/0021_message_reply_forward.down.sql
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
DROP INDEX IF EXISTS message_boxes_reply_lookup_idx;
|
||||
|
||||
ALTER TABLE message_boxes
|
||||
DROP COLUMN IF EXISTS fwd_date,
|
||||
DROP COLUMN IF EXISTS fwd_from_name,
|
||||
DROP COLUMN IF EXISTS fwd_from_peer_id,
|
||||
DROP COLUMN IF EXISTS fwd_from_peer_type,
|
||||
DROP COLUMN IF EXISTS quote_offset,
|
||||
DROP COLUMN IF EXISTS quote_entities,
|
||||
DROP COLUMN IF EXISTS quote_text,
|
||||
DROP COLUMN IF EXISTS reply_to_top_id,
|
||||
DROP COLUMN IF EXISTS reply_to_peer_id,
|
||||
DROP COLUMN IF EXISTS reply_to_peer_type,
|
||||
DROP COLUMN IF EXISTS reply_to_msg_id,
|
||||
DROP COLUMN IF EXISTS noforwards,
|
||||
DROP COLUMN IF EXISTS silent;
|
||||
|
||||
ALTER TABLE private_messages
|
||||
DROP COLUMN IF EXISTS fwd_date,
|
||||
DROP COLUMN IF EXISTS fwd_from_name,
|
||||
DROP COLUMN IF EXISTS fwd_from_peer_id,
|
||||
DROP COLUMN IF EXISTS fwd_from_peer_type,
|
||||
DROP COLUMN IF EXISTS quote_offset,
|
||||
DROP COLUMN IF EXISTS quote_entities,
|
||||
DROP COLUMN IF EXISTS quote_text,
|
||||
DROP COLUMN IF EXISTS reply_to_top_id,
|
||||
DROP COLUMN IF EXISTS reply_to_peer_id,
|
||||
DROP COLUMN IF EXISTS reply_to_peer_type,
|
||||
DROP COLUMN IF EXISTS reply_to_msg_id,
|
||||
DROP COLUMN IF EXISTS noforwards,
|
||||
DROP COLUMN IF EXISTS silent;
|
||||
35
deploy/migrations/0021_message_reply_forward.up.sql
Normal file
35
deploy/migrations/0021_message_reply_forward.up.sql
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
-- 0021_message_reply_forward: private-message silent/reply/forward metadata.
|
||||
|
||||
ALTER TABLE private_messages
|
||||
ADD COLUMN IF NOT EXISTS silent BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS noforwards BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS reply_to_msg_id INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS reply_to_peer_type VARCHAR(16) NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS reply_to_peer_id BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS reply_to_top_id INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS quote_text TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS quote_entities JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS quote_offset INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS fwd_from_peer_type VARCHAR(16) NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS fwd_from_peer_id BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS fwd_from_name TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS fwd_date INT NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE message_boxes
|
||||
ADD COLUMN IF NOT EXISTS silent BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS noforwards BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS reply_to_msg_id INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS reply_to_peer_type VARCHAR(16) NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS reply_to_peer_id BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS reply_to_top_id INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS quote_text TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS quote_entities JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS quote_offset INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS fwd_from_peer_type VARCHAR(16) NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS fwd_from_peer_id BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS fwd_from_name TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS fwd_date INT NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS message_boxes_reply_lookup_idx
|
||||
ON message_boxes (owner_user_id, peer_type, peer_id, box_id)
|
||||
WHERE NOT deleted;
|
||||
9
deploy/migrations/0022_channels.down.sql
Normal file
9
deploy/migrations/0022_channels.down.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
DROP TABLE IF EXISTS channel_invites CASCADE;
|
||||
DROP TABLE IF EXISTS channel_dialogs CASCADE;
|
||||
DROP TABLE IF EXISTS channel_admin_log_events CASCADE;
|
||||
DROP TABLE IF EXISTS channel_update_events CASCADE;
|
||||
DROP TABLE IF EXISTS channel_messages CASCADE;
|
||||
DROP TABLE IF EXISTS channel_members CASCADE;
|
||||
DROP TABLE IF EXISTS channel_usernames CASCADE;
|
||||
DROP TABLE IF EXISTS channel_invite_hashes CASCADE;
|
||||
DROP TABLE IF EXISTS channels CASCADE;
|
||||
398
deploy/migrations/0022_channels.up.sql
Normal file
398
deploy/migrations/0022_channels.up.sql
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
-- 0022_channels: supergroup/channel storage.
|
||||
--
|
||||
-- Channel messages are single-copy. Per-user dialog/read state is stored separately.
|
||||
-- Channel pts is scoped by channel_id and persisted in channel_update_events.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channels (
|
||||
id BIGINT NOT NULL,
|
||||
access_hash BIGINT NOT NULL,
|
||||
creator_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
title TEXT NOT NULL,
|
||||
about TEXT NOT NULL DEFAULT '',
|
||||
username TEXT,
|
||||
broadcast BOOLEAN NOT NULL DEFAULT false,
|
||||
megagroup BOOLEAN NOT NULL DEFAULT false,
|
||||
forum BOOLEAN NOT NULL DEFAULT false,
|
||||
forum_tabs BOOLEAN NOT NULL DEFAULT false,
|
||||
noforwards BOOLEAN NOT NULL DEFAULT false,
|
||||
join_to_send BOOLEAN NOT NULL DEFAULT false,
|
||||
join_request BOOLEAN NOT NULL DEFAULT false,
|
||||
signatures BOOLEAN NOT NULL DEFAULT false,
|
||||
pre_history_hidden BOOLEAN NOT NULL DEFAULT false,
|
||||
participants_hidden BOOLEAN NOT NULL DEFAULT false,
|
||||
antispam BOOLEAN NOT NULL DEFAULT false,
|
||||
linked_chat_id BIGINT NOT NULL DEFAULT 0,
|
||||
slowmode_seconds INT NOT NULL DEFAULT 0,
|
||||
default_banned_rights JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
available_reactions JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
color_set BOOLEAN NOT NULL DEFAULT false,
|
||||
color INT NOT NULL DEFAULT 0,
|
||||
color_background_emoji_id BIGINT NOT NULL DEFAULT 0,
|
||||
profile_color_set BOOLEAN NOT NULL DEFAULT false,
|
||||
profile_color INT NOT NULL DEFAULT 0,
|
||||
profile_color_background_emoji_id BIGINT NOT NULL DEFAULT 0,
|
||||
emoji_status_document_id BIGINT NOT NULL DEFAULT 0,
|
||||
emoji_status_until INT NOT NULL DEFAULT 0,
|
||||
participants_count INT NOT NULL DEFAULT 0,
|
||||
admins_count INT NOT NULL DEFAULT 0,
|
||||
kicked_count INT NOT NULL DEFAULT 0,
|
||||
banned_count INT NOT NULL DEFAULT 0,
|
||||
top_message_id INT NOT NULL DEFAULT 0,
|
||||
pts INT NOT NULL DEFAULT 0,
|
||||
admin_log_seq BIGINT NOT NULL DEFAULT 0,
|
||||
ttl_period INT NOT NULL DEFAULT 0,
|
||||
date INT NOT NULL,
|
||||
deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT channels_kind_check CHECK (
|
||||
((broadcast AND NOT megagroup AND NOT forum)
|
||||
OR (megagroup AND NOT broadcast))
|
||||
AND (NOT forum_tabs OR forum)
|
||||
),
|
||||
CONSTRAINT channels_title_nonempty_check CHECK (title <> '')
|
||||
) PARTITION BY HASH (id);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS channels_access_hash_idx
|
||||
ON channels (id, access_hash);
|
||||
CREATE INDEX IF NOT EXISTS channels_creator_idx
|
||||
ON channels (creator_user_id, id DESC)
|
||||
WHERE NOT deleted;
|
||||
CREATE INDEX IF NOT EXISTS channels_linked_chat_idx
|
||||
ON channels (linked_chat_id)
|
||||
WHERE linked_chat_id <> 0 AND NOT deleted;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS channels_p%s PARTITION OF channels FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- PostgreSQL global unique indexes on partitioned tables must include the partition key.
|
||||
-- Keep username uniqueness in a compact lookup table instead of relying on channels(username).
|
||||
CREATE TABLE IF NOT EXISTS channel_usernames (
|
||||
username_lower TEXT PRIMARY KEY,
|
||||
channel_id BIGINT NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT channel_usernames_nonempty_check CHECK (username_lower <> '')
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_members (
|
||||
channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
inviter_user_id BIGINT NOT NULL DEFAULT 0,
|
||||
role VARCHAR(16) NOT NULL DEFAULT 'member',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
joined_at INT NOT NULL DEFAULT 0,
|
||||
left_at INT NOT NULL DEFAULT 0,
|
||||
admin_rights JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
banned_rights JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
rank TEXT NOT NULL DEFAULT '',
|
||||
available_min_id INT NOT NULL DEFAULT 0,
|
||||
available_min_pts INT NOT NULL DEFAULT 0,
|
||||
read_inbox_max_id INT NOT NULL DEFAULT 0,
|
||||
read_inbox_date INT NOT NULL DEFAULT 0,
|
||||
read_outbox_max_id INT NOT NULL DEFAULT 0,
|
||||
unread_mark BOOLEAN NOT NULL DEFAULT false,
|
||||
slowmode_last_send_date INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (channel_id, user_id),
|
||||
CONSTRAINT channel_members_role_check CHECK (role IN ('creator', 'admin', 'member')),
|
||||
CONSTRAINT channel_members_status_check CHECK (status IN ('active', 'left', 'kicked', 'banned'))
|
||||
) PARTITION BY HASH (channel_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS channel_members_user_active_idx
|
||||
ON channel_members (user_id, channel_id)
|
||||
WHERE status = 'active';
|
||||
CREATE INDEX IF NOT EXISTS channel_members_user_left_idx
|
||||
ON channel_members (user_id, left_at DESC, channel_id DESC)
|
||||
WHERE status = 'left';
|
||||
CREATE INDEX IF NOT EXISTS channel_members_channel_role_idx
|
||||
ON channel_members (channel_id, role, user_id)
|
||||
WHERE status = 'active';
|
||||
CREATE INDEX IF NOT EXISTS channel_members_read_participants_idx
|
||||
ON channel_members (channel_id, read_inbox_max_id, user_id)
|
||||
WHERE status = 'active';
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS channel_members_p%s PARTITION OF channel_members FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_messages (
|
||||
channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
id INT NOT NULL,
|
||||
random_id BIGINT NOT NULL DEFAULT 0,
|
||||
sender_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
from_peer_type VARCHAR(16) NOT NULL DEFAULT 'user',
|
||||
from_peer_id BIGINT NOT NULL,
|
||||
send_as_peer_type VARCHAR(16),
|
||||
send_as_peer_id BIGINT,
|
||||
message_date INT NOT NULL,
|
||||
edit_date INT NOT NULL DEFAULT 0,
|
||||
post BOOLEAN NOT NULL DEFAULT false,
|
||||
silent BOOLEAN NOT NULL DEFAULT false,
|
||||
noforwards BOOLEAN NOT NULL DEFAULT false,
|
||||
body TEXT NOT NULL DEFAULT '',
|
||||
entities JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
reply_to JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
reply_to_msg_id INT NOT NULL DEFAULT 0,
|
||||
reply_to_peer_type VARCHAR(16) NOT NULL DEFAULT '',
|
||||
reply_to_peer_id BIGINT NOT NULL DEFAULT 0,
|
||||
reply_to_top_id INT NOT NULL DEFAULT 0,
|
||||
fwd_from JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
discussion_channel_id BIGINT NOT NULL DEFAULT 0,
|
||||
discussion_message_id INT NOT NULL DEFAULT 0,
|
||||
action JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
pts INT NOT NULL,
|
||||
deleted BOOLEAN NOT NULL DEFAULT false,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (channel_id, id),
|
||||
CONSTRAINT channel_messages_peer_type_check CHECK (
|
||||
from_peer_type IN ('user', 'channel')
|
||||
AND (send_as_peer_type IS NULL OR send_as_peer_type IN ('user', 'channel'))
|
||||
AND (reply_to_peer_type = '' OR reply_to_peer_type IN ('user', 'channel'))
|
||||
),
|
||||
CONSTRAINT channel_messages_content_check CHECK (body <> '' OR action <> '{}'::jsonb)
|
||||
) PARTITION BY HASH (channel_id);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS channel_messages_random_idx
|
||||
ON channel_messages (channel_id, sender_user_id, random_id)
|
||||
WHERE random_id <> 0;
|
||||
CREATE INDEX IF NOT EXISTS channel_messages_history_idx
|
||||
ON channel_messages (channel_id, id DESC)
|
||||
WHERE NOT deleted;
|
||||
CREATE INDEX IF NOT EXISTS channel_messages_sender_history_idx
|
||||
ON channel_messages (channel_id, sender_user_id, id DESC)
|
||||
WHERE NOT deleted;
|
||||
CREATE INDEX IF NOT EXISTS channel_messages_date_idx
|
||||
ON channel_messages (channel_id, message_date DESC, id DESC)
|
||||
WHERE NOT deleted;
|
||||
CREATE INDEX IF NOT EXISTS channel_messages_reply_thread_idx
|
||||
ON channel_messages (channel_id, reply_to_top_id, id DESC)
|
||||
WHERE reply_to_top_id > 0 AND NOT deleted;
|
||||
CREATE INDEX IF NOT EXISTS channel_messages_discussion_ref_idx
|
||||
ON channel_messages (discussion_channel_id, discussion_message_id)
|
||||
WHERE discussion_channel_id <> 0 AND discussion_message_id <> 0 AND NOT deleted;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS channel_messages_p%s PARTITION OF channel_messages FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_update_events (
|
||||
channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
pts INT NOT NULL,
|
||||
pts_count INT NOT NULL DEFAULT 1,
|
||||
date INT NOT NULL,
|
||||
event_type VARCHAR(32) NOT NULL,
|
||||
message_id INT NOT NULL DEFAULT 0,
|
||||
message_ids JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
sender_user_id BIGINT NOT NULL DEFAULT 0,
|
||||
user_ids JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (channel_id, pts),
|
||||
CONSTRAINT channel_update_events_pts_count_check CHECK (pts_count > 0),
|
||||
CONSTRAINT channel_update_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'new_channel_message',
|
||||
'edit_channel_message',
|
||||
'delete_channel_messages',
|
||||
'channel_participant',
|
||||
'pinned_channel_messages',
|
||||
'noop'
|
||||
)
|
||||
)
|
||||
) PARTITION BY HASH (channel_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS channel_update_events_scan_idx
|
||||
ON channel_update_events (channel_id, pts ASC);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS channel_update_events_p%s PARTITION OF channel_update_events FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_admin_log_events (
|
||||
channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
id BIGINT NOT NULL,
|
||||
actor_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
event_date INT NOT NULL,
|
||||
event_type VARCHAR(48) NOT NULL,
|
||||
prev_string TEXT NOT NULL DEFAULT '',
|
||||
new_string TEXT NOT NULL DEFAULT '',
|
||||
prev_bool BOOLEAN NOT NULL DEFAULT false,
|
||||
new_bool BOOLEAN NOT NULL DEFAULT false,
|
||||
prev_int INT NOT NULL DEFAULT 0,
|
||||
new_int INT NOT NULL DEFAULT 0,
|
||||
prev_participant JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
new_participant JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
participant JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
message JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
prev_message JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
new_message JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
query TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (channel_id, id),
|
||||
CONSTRAINT channel_admin_log_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'change_title',
|
||||
'change_username',
|
||||
'change_linked_chat',
|
||||
'toggle_signatures',
|
||||
'toggle_pre_history_hidden',
|
||||
'toggle_forum',
|
||||
'toggle_anti_spam',
|
||||
'toggle_slow_mode',
|
||||
'participant_invite',
|
||||
'participant_join',
|
||||
'participant_leave',
|
||||
'participant_promote',
|
||||
'participant_demote',
|
||||
'participant_ban',
|
||||
'participant_unban',
|
||||
'participant_kick',
|
||||
'participant_unkick',
|
||||
'update_pinned',
|
||||
'send_message',
|
||||
'edit_message',
|
||||
'delete_message'
|
||||
)
|
||||
)
|
||||
) PARTITION BY HASH (channel_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS channel_admin_log_events_scan_idx
|
||||
ON channel_admin_log_events (channel_id, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS channel_admin_log_events_actor_idx
|
||||
ON channel_admin_log_events (channel_id, actor_user_id, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS channel_admin_log_events_type_idx
|
||||
ON channel_admin_log_events (channel_id, event_type, id DESC);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS channel_admin_log_events_p%s PARTITION OF channel_admin_log_events FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_dialogs (
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
folder_id INT NOT NULL DEFAULT 0,
|
||||
top_message_id INT NOT NULL DEFAULT 0,
|
||||
top_message_date INT NOT NULL DEFAULT 0,
|
||||
read_inbox_max_id INT NOT NULL DEFAULT 0,
|
||||
read_outbox_max_id INT NOT NULL DEFAULT 0,
|
||||
unread_count INT NOT NULL DEFAULT 0,
|
||||
unread_mentions_count INT NOT NULL DEFAULT 0,
|
||||
unread_reactions_count INT NOT NULL DEFAULT 0,
|
||||
pinned BOOLEAN NOT NULL DEFAULT false,
|
||||
pinned_order INT NOT NULL DEFAULT 0,
|
||||
unread_mark BOOLEAN NOT NULL DEFAULT false,
|
||||
view_forum_as_messages BOOLEAN NOT NULL DEFAULT false,
|
||||
notify_settings JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (user_id, channel_id),
|
||||
CONSTRAINT channel_dialogs_folder_id_check CHECK (folder_id >= 0)
|
||||
) PARTITION BY HASH (user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS channel_dialogs_user_top_idx
|
||||
ON channel_dialogs (user_id, folder_id, pinned DESC, pinned_order DESC, top_message_date DESC, top_message_id DESC, channel_id DESC);
|
||||
CREATE INDEX IF NOT EXISTS channel_dialogs_pinned_idx
|
||||
ON channel_dialogs (user_id, pinned)
|
||||
WHERE pinned;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS channel_dialogs_p%s PARTITION OF channel_dialogs FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_invites (
|
||||
channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
invite_id BIGINT NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
admin_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
permanent BOOLEAN NOT NULL DEFAULT false,
|
||||
revoked BOOLEAN NOT NULL DEFAULT false,
|
||||
request_needed BOOLEAN NOT NULL DEFAULT false,
|
||||
expire_date INT,
|
||||
usage_limit INT,
|
||||
usage_count INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (channel_id, invite_id)
|
||||
) PARTITION BY HASH (channel_id);
|
||||
|
||||
-- Keep invite hash uniqueness outside the partitioned table because PostgreSQL
|
||||
-- requires global unique indexes on partitions to include the partition key.
|
||||
CREATE TABLE IF NOT EXISTS channel_invite_hashes (
|
||||
hash TEXT PRIMARY KEY,
|
||||
channel_id BIGINT NOT NULL,
|
||||
invite_id BIGINT NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT channel_invite_hashes_nonempty_check CHECK (hash <> '')
|
||||
);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS channel_invites_p%s PARTITION OF channel_invites FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
18
deploy/migrations/0023_channel_admin_invites.down.sql
Normal file
18
deploy/migrations/0023_channel_admin_invites.down.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
DROP INDEX IF EXISTS channel_invites_hash_lookup_idx;
|
||||
|
||||
ALTER TABLE channel_update_events
|
||||
DROP CONSTRAINT IF EXISTS channel_update_events_type_check;
|
||||
|
||||
ALTER TABLE channel_update_events
|
||||
ADD CONSTRAINT channel_update_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'new_channel_message',
|
||||
'edit_channel_message',
|
||||
'delete_channel_messages',
|
||||
'channel_participant',
|
||||
'noop'
|
||||
)
|
||||
);
|
||||
|
||||
ALTER TABLE channels
|
||||
DROP COLUMN IF EXISTS pinned_message_id;
|
||||
22
deploy/migrations/0023_channel_admin_invites.up.sql
Normal file
22
deploy/migrations/0023_channel_admin_invites.up.sql
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
-- 0023_channel_admin_invites: metadata needed by channel admin/pin/invite RPCs.
|
||||
|
||||
ALTER TABLE channels
|
||||
ADD COLUMN IF NOT EXISTS pinned_message_id INT NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE channel_update_events
|
||||
DROP CONSTRAINT IF EXISTS channel_update_events_type_check;
|
||||
|
||||
ALTER TABLE channel_update_events
|
||||
ADD CONSTRAINT channel_update_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'new_channel_message',
|
||||
'edit_channel_message',
|
||||
'delete_channel_messages',
|
||||
'channel_participant',
|
||||
'pinned_channel_messages',
|
||||
'noop'
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS channel_invites_hash_lookup_idx
|
||||
ON channel_invite_hashes (hash, channel_id, invite_id);
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
-- No-op by design.
|
||||
--
|
||||
-- These columns/table are part of the fresh 0022 channel schema. This migration
|
||||
-- only backfills older developer databases that had already applied an earlier
|
||||
-- 0022 draft, so dropping the objects on a one-step rollback would corrupt fresh
|
||||
-- schemas where 0022 legitimately owns them.
|
||||
78
deploy/migrations/0024_channel_admin_log_backfill.up.sql
Normal file
78
deploy/migrations/0024_channel_admin_log_backfill.up.sql
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
-- 0024_channel_admin_log_backfill: bring existing developer DBs forward after
|
||||
-- channel settings/admin-log fields were added to the initial 0022 draft.
|
||||
--
|
||||
-- Fresh databases already get these objects from 0022; every statement here is
|
||||
-- idempotent so old local databases can migrate without reset.
|
||||
|
||||
ALTER TABLE channels
|
||||
ADD COLUMN IF NOT EXISTS pre_history_hidden BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS slowmode_seconds INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS admin_log_seq BIGINT NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_admin_log_events (
|
||||
channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
id BIGINT NOT NULL,
|
||||
actor_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
event_date INT NOT NULL,
|
||||
event_type VARCHAR(48) NOT NULL,
|
||||
prev_string TEXT NOT NULL DEFAULT '',
|
||||
new_string TEXT NOT NULL DEFAULT '',
|
||||
prev_bool BOOLEAN NOT NULL DEFAULT false,
|
||||
new_bool BOOLEAN NOT NULL DEFAULT false,
|
||||
prev_int INT NOT NULL DEFAULT 0,
|
||||
new_int INT NOT NULL DEFAULT 0,
|
||||
prev_participant JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
new_participant JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
participant JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
message JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
prev_message JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
new_message JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
query TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (channel_id, id),
|
||||
CONSTRAINT channel_admin_log_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'change_title',
|
||||
'change_username',
|
||||
'change_linked_chat',
|
||||
'toggle_signatures',
|
||||
'toggle_pre_history_hidden',
|
||||
'toggle_forum',
|
||||
'toggle_anti_spam',
|
||||
'toggle_slow_mode',
|
||||
'participant_invite',
|
||||
'participant_join',
|
||||
'participant_leave',
|
||||
'participant_promote',
|
||||
'participant_demote',
|
||||
'participant_ban',
|
||||
'participant_unban',
|
||||
'participant_kick',
|
||||
'participant_unkick',
|
||||
'update_pinned',
|
||||
'send_message',
|
||||
'edit_message',
|
||||
'delete_message'
|
||||
)
|
||||
)
|
||||
) PARTITION BY HASH (channel_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS channel_admin_log_events_scan_idx
|
||||
ON channel_admin_log_events (channel_id, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS channel_admin_log_events_actor_idx
|
||||
ON channel_admin_log_events (channel_id, actor_user_id, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS channel_admin_log_events_type_idx
|
||||
ON channel_admin_log_events (channel_id, event_type, id DESC);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS channel_admin_log_events_p%s PARTITION OF channel_admin_log_events FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
-- No-op by design.
|
||||
--
|
||||
-- Fresh databases own this column from 0022; this migration only repairs older
|
||||
-- developer databases that had already applied an earlier 0022 draft.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
-- 0025_channel_member_slowmode_backfill: idempotent compatibility migration for
|
||||
-- developer databases that applied an earlier 0022 channel schema draft.
|
||||
|
||||
ALTER TABLE channel_members
|
||||
ADD COLUMN IF NOT EXISTS slowmode_last_send_date INT NOT NULL DEFAULT 0;
|
||||
|
|
@ -0,0 +1 @@
|
|||
-- No-op: read receipt dates and TDesktop app config keys are forward-compatible.
|
||||
19
deploy/migrations/0026_channel_read_participants.up.sql
Normal file
19
deploy/migrations/0026_channel_read_participants.up.sql
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
-- 0026_channel_read_participants: store channel read receipt dates and expose TDesktop read mark config.
|
||||
|
||||
ALTER TABLE channel_members
|
||||
ADD COLUMN IF NOT EXISTS read_inbox_date INT NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS channel_members_read_participants_idx
|
||||
ON channel_members (channel_id, read_inbox_max_id, user_id)
|
||||
WHERE status = 'active';
|
||||
|
||||
UPDATE app_configs
|
||||
SET hash = 2,
|
||||
config_json = config_json
|
||||
|| jsonb_build_object(
|
||||
'chat_read_mark_size_threshold', 50,
|
||||
'chat_read_mark_expire_period', 604800,
|
||||
'pm_read_date_expire_period', 604800
|
||||
),
|
||||
updated_at = now()
|
||||
WHERE client = 'tdesktop';
|
||||
|
|
@ -0,0 +1 @@
|
|||
DROP INDEX IF EXISTS channel_messages_body_trgm_idx;
|
||||
10
deploy/migrations/0027_channel_message_search_indexes.up.sql
Normal file
10
deploy/migrations/0027_channel_message_search_indexes.up.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-- 0027_channel_message_search_indexes: bounded in-channel text search.
|
||||
--
|
||||
-- TDesktop uses messages.search with inputPeerChannel for in-chat search.
|
||||
-- Keep text lookup indexed on the single-copy channel_messages table.
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS channel_messages_body_trgm_idx
|
||||
ON channel_messages USING gin (body gin_trgm_ops)
|
||||
WHERE NOT deleted AND body <> '';
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
-- No-op by design.
|
||||
--
|
||||
-- Fresh databases own this column from 0022; this migration only repairs older
|
||||
-- developer databases that had already applied an earlier 0022 draft.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
-- 0028_channel_noforwards_backfill: ensure older developer databases have
|
||||
-- the channel content-protection flag used by messages.toggleNoForwards.
|
||||
--
|
||||
-- Fresh databases already get this column from 0022.
|
||||
|
||||
ALTER TABLE channels
|
||||
ADD COLUMN IF NOT EXISTS noforwards BOOLEAN NOT NULL DEFAULT false;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
-- No-op by design.
|
||||
--
|
||||
-- Fresh databases own this column from 0022; this migration only repairs older
|
||||
-- developer databases that had already applied an earlier 0022 draft.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
-- 0029_channel_available_reactions: persisted reaction policy for
|
||||
-- messages.setChatAvailableReactions and channels.getFullChannel.
|
||||
--
|
||||
-- Fresh databases already get this column from 0022.
|
||||
|
||||
ALTER TABLE channels
|
||||
ADD COLUMN IF NOT EXISTS available_reactions JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
-- 0030_user_update_channel_peers rollback.
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP CONSTRAINT IF EXISTS user_update_events_peer_type_check;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD CONSTRAINT user_update_events_peer_type_check
|
||||
CHECK (peer_type IS NULL OR peer_type IN ('user'));
|
||||
8
deploy/migrations/0030_user_update_channel_peers.up.sql
Normal file
8
deploy/migrations/0030_user_update_channel_peers.up.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- 0030_user_update_channel_peers: allow account-level update events to reference channel peers.
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP CONSTRAINT IF EXISTS user_update_events_peer_type_check;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD CONSTRAINT user_update_events_peer_type_check
|
||||
CHECK (peer_type IS NULL OR peer_type IN ('user', 'channel'));
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE channel_members
|
||||
DROP COLUMN IF EXISTS available_min_pts;
|
||||
4
deploy/migrations/0031_channel_available_min_pts.up.sql
Normal file
4
deploy/migrations/0031_channel_available_min_pts.up.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
-- 0031_channel_available_min_pts: channelDifference visibility floor per member.
|
||||
|
||||
ALTER TABLE channel_members
|
||||
ADD COLUMN IF NOT EXISTS available_min_pts INT NOT NULL DEFAULT 0;
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
ALTER TABLE user_update_events
|
||||
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD CONSTRAINT user_update_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'new_message',
|
||||
'read_history_inbox',
|
||||
'read_history_outbox',
|
||||
'edit_message',
|
||||
'contacts_reset',
|
||||
'dialog_pinned',
|
||||
'pinned_dialogs',
|
||||
'dialog_unread_mark',
|
||||
'peer_settings',
|
||||
'delete_messages',
|
||||
'dialog_filter',
|
||||
'dialog_filter_order',
|
||||
'dialog_filters',
|
||||
'folder_peers',
|
||||
'noop'
|
||||
)
|
||||
);
|
||||
26
deploy/migrations/0032_user_update_channel_available.up.sql
Normal file
26
deploy/migrations/0032_user_update_channel_available.up.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
-- 0032_user_update_channel_available: durable account update for channel local clear.
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
|
||||
|
||||
ALTER TABLE user_update_events
|
||||
ADD CONSTRAINT user_update_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'new_message',
|
||||
'read_history_inbox',
|
||||
'read_history_outbox',
|
||||
'edit_message',
|
||||
'contacts_reset',
|
||||
'dialog_pinned',
|
||||
'pinned_dialogs',
|
||||
'dialog_unread_mark',
|
||||
'peer_settings',
|
||||
'delete_messages',
|
||||
'dialog_filter',
|
||||
'dialog_filter_order',
|
||||
'dialog_filters',
|
||||
'folder_peers',
|
||||
'channel_available_messages',
|
||||
'noop'
|
||||
)
|
||||
);
|
||||
7
deploy/migrations/0033_app_config_quote_length.down.sql
Normal file
7
deploy/migrations/0033_app_config_quote_length.down.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
-- 0033_app_config_quote_length rollback.
|
||||
|
||||
UPDATE app_configs
|
||||
SET hash = 2,
|
||||
config_json = config_json - 'quote_length_max',
|
||||
updated_at = now()
|
||||
WHERE client = 'tdesktop';
|
||||
7
deploy/migrations/0033_app_config_quote_length.up.sql
Normal file
7
deploy/migrations/0033_app_config_quote_length.up.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
-- 0033_app_config_quote_length: expose TDesktop quote reply length limit explicitly.
|
||||
|
||||
UPDATE app_configs
|
||||
SET hash = 3,
|
||||
config_json = config_json || jsonb_build_object('quote_length_max', 1024),
|
||||
updated_at = now()
|
||||
WHERE client = 'tdesktop';
|
||||
6
deploy/migrations/0034_channel_invite_importers.down.sql
Normal file
6
deploy/migrations/0034_channel_invite_importers.down.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-- 0034_channel_invite_importers rollback.
|
||||
|
||||
DROP TABLE IF EXISTS channel_invite_importers;
|
||||
|
||||
ALTER TABLE channel_invites
|
||||
DROP COLUMN IF EXISTS requested_count;
|
||||
37
deploy/migrations/0034_channel_invite_importers.up.sql
Normal file
37
deploy/migrations/0034_channel_invite_importers.up.sql
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
-- 0034_channel_invite_importers: invite management read model.
|
||||
|
||||
ALTER TABLE channel_invites
|
||||
ADD COLUMN IF NOT EXISTS requested_count INT NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_invite_importers (
|
||||
channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
invite_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
date INT NOT NULL,
|
||||
requested BOOLEAN NOT NULL DEFAULT false,
|
||||
approved_by BIGINT NOT NULL DEFAULT 0,
|
||||
via_chatlist BOOLEAN NOT NULL DEFAULT false,
|
||||
about TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (channel_id, user_id)
|
||||
) PARTITION BY HASH (channel_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS channel_invite_importers_link_idx
|
||||
ON channel_invite_importers (channel_id, invite_id, requested, date DESC, user_id DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS channel_invite_importers_requested_idx
|
||||
ON channel_invite_importers (channel_id, requested, date DESC, user_id DESC);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS channel_invite_importers_p%s PARTITION OF channel_invite_importers FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
5
deploy/migrations/0035_channel_join_settings.down.sql
Normal file
5
deploy/migrations/0035_channel_join_settings.down.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-- 0035_channel_join_settings rollback.
|
||||
|
||||
ALTER TABLE channels
|
||||
DROP COLUMN IF EXISTS join_request,
|
||||
DROP COLUMN IF EXISTS join_to_send;
|
||||
5
deploy/migrations/0035_channel_join_settings.up.sql
Normal file
5
deploy/migrations/0035_channel_join_settings.up.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-- 0035_channel_join_settings: public join/send gating flags.
|
||||
|
||||
ALTER TABLE channels
|
||||
ADD COLUMN IF NOT EXISTS join_to_send BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS join_request BOOLEAN NOT NULL DEFAULT false;
|
||||
4
deploy/migrations/0036_channel_discussion_links.down.sql
Normal file
4
deploy/migrations/0036_channel_discussion_links.down.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
DROP INDEX IF EXISTS channels_linked_chat_idx;
|
||||
|
||||
ALTER TABLE channels
|
||||
DROP COLUMN IF EXISTS linked_chat_id;
|
||||
8
deploy/migrations/0036_channel_discussion_links.up.sql
Normal file
8
deploy/migrations/0036_channel_discussion_links.up.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- 0036_channel_discussion_links: persist bidirectional broadcast <-> discussion group links.
|
||||
|
||||
ALTER TABLE channels
|
||||
ADD COLUMN IF NOT EXISTS linked_chat_id BIGINT NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS channels_linked_chat_idx
|
||||
ON channels (linked_chat_id)
|
||||
WHERE linked_chat_id <> 0 AND NOT deleted;
|
||||
10
deploy/migrations/0037_channel_message_threads.down.sql
Normal file
10
deploy/migrations/0037_channel_message_threads.down.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
DROP INDEX IF EXISTS channel_messages_discussion_ref_idx;
|
||||
DROP INDEX IF EXISTS channel_messages_reply_thread_idx;
|
||||
|
||||
ALTER TABLE channel_messages
|
||||
DROP COLUMN IF EXISTS discussion_message_id,
|
||||
DROP COLUMN IF EXISTS discussion_channel_id,
|
||||
DROP COLUMN IF EXISTS reply_to_top_id,
|
||||
DROP COLUMN IF EXISTS reply_to_peer_id,
|
||||
DROP COLUMN IF EXISTS reply_to_peer_type,
|
||||
DROP COLUMN IF EXISTS reply_to_msg_id;
|
||||
36
deploy/migrations/0037_channel_message_threads.up.sql
Normal file
36
deploy/migrations/0037_channel_message_threads.up.sql
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
ALTER TABLE channel_messages
|
||||
ADD COLUMN IF NOT EXISTS reply_to_msg_id INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS reply_to_peer_type VARCHAR(16) NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS reply_to_peer_id BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS reply_to_top_id INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS discussion_channel_id BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS discussion_message_id INT NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE channel_messages
|
||||
SET
|
||||
reply_to_msg_id = CASE
|
||||
WHEN (reply_to ->> 'MessageID') ~ '^[0-9]+$' THEN (reply_to ->> 'MessageID')::INT
|
||||
ELSE reply_to_msg_id
|
||||
END,
|
||||
reply_to_peer_type = COALESCE(NULLIF(reply_to #>> '{Peer,Type}', ''), reply_to_peer_type),
|
||||
reply_to_peer_id = CASE
|
||||
WHEN (reply_to #>> '{Peer,ID}') ~ '^[0-9]+$' THEN (reply_to #>> '{Peer,ID}')::BIGINT
|
||||
ELSE reply_to_peer_id
|
||||
END,
|
||||
reply_to_top_id = CASE
|
||||
WHEN (reply_to ->> 'TopMessageID') ~ '^[0-9]+$' AND (reply_to ->> 'TopMessageID')::INT > 0
|
||||
THEN (reply_to ->> 'TopMessageID')::INT
|
||||
WHEN (reply_to ->> 'MessageID') ~ '^[0-9]+$'
|
||||
THEN (reply_to ->> 'MessageID')::INT
|
||||
ELSE reply_to_top_id
|
||||
END
|
||||
WHERE reply_to <> '{}'::jsonb
|
||||
AND reply_to_msg_id = 0;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS channel_messages_reply_thread_idx
|
||||
ON channel_messages (channel_id, reply_to_top_id, id DESC)
|
||||
WHERE reply_to_top_id > 0 AND NOT deleted;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS channel_messages_discussion_ref_idx
|
||||
ON channel_messages (discussion_channel_id, discussion_message_id)
|
||||
WHERE discussion_channel_id <> 0 AND discussion_message_id <> 0 AND NOT deleted;
|
||||
6
deploy/migrations/0038_channel_default_send_as.down.sql
Normal file
6
deploy/migrations/0038_channel_default_send_as.down.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
ALTER TABLE channel_dialogs
|
||||
DROP CONSTRAINT IF EXISTS channel_dialogs_default_send_as_peer_type_check;
|
||||
|
||||
ALTER TABLE channel_dialogs
|
||||
DROP COLUMN IF EXISTS default_send_as_peer_id,
|
||||
DROP COLUMN IF EXISTS default_send_as_peer_type;
|
||||
13
deploy/migrations/0038_channel_default_send_as.up.sql
Normal file
13
deploy/migrations/0038_channel_default_send_as.up.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
ALTER TABLE channel_dialogs
|
||||
ADD COLUMN IF NOT EXISTS default_send_as_peer_type VARCHAR(16),
|
||||
ADD COLUMN IF NOT EXISTS default_send_as_peer_id BIGINT;
|
||||
|
||||
ALTER TABLE channel_dialogs
|
||||
DROP CONSTRAINT IF EXISTS channel_dialogs_default_send_as_peer_type_check;
|
||||
|
||||
ALTER TABLE channel_dialogs
|
||||
ADD CONSTRAINT channel_dialogs_default_send_as_peer_type_check
|
||||
CHECK (
|
||||
default_send_as_peer_type IS NULL
|
||||
OR default_send_as_peer_type IN ('user', 'channel')
|
||||
);
|
||||
4
deploy/migrations/0039_channel_message_views.down.sql
Normal file
4
deploy/migrations/0039_channel_message_views.down.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
DROP TABLE IF EXISTS channel_message_viewers CASCADE;
|
||||
|
||||
ALTER TABLE channel_messages
|
||||
DROP COLUMN IF EXISTS views_count;
|
||||
27
deploy/migrations/0039_channel_message_views.up.sql
Normal file
27
deploy/migrations/0039_channel_message_views.up.sql
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
ALTER TABLE channel_messages
|
||||
ADD COLUMN IF NOT EXISTS views_count INT NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_message_viewers (
|
||||
channel_id BIGINT NOT NULL,
|
||||
message_id INT NOT NULL,
|
||||
viewer_user_id BIGINT NOT NULL,
|
||||
viewed_at INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (channel_id, message_id, viewer_user_id),
|
||||
CONSTRAINT channel_message_viewers_positive_check CHECK (
|
||||
channel_id > 0 AND message_id > 0 AND viewer_user_id > 0
|
||||
)
|
||||
) PARTITION BY HASH (channel_id);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS channel_message_viewers_p%s PARTITION OF channel_message_viewers FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
1
deploy/migrations/0040_channel_unread_mentions.down.sql
Normal file
1
deploy/migrations/0040_channel_unread_mentions.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE IF EXISTS channel_unread_mentions CASCADE;
|
||||
25
deploy/migrations/0040_channel_unread_mentions.up.sql
Normal file
25
deploy/migrations/0040_channel_unread_mentions.up.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
CREATE TABLE IF NOT EXISTS channel_unread_mentions (
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
message_id INT NOT NULL,
|
||||
top_message_id INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (user_id, channel_id, message_id),
|
||||
FOREIGN KEY (channel_id, message_id) REFERENCES channel_messages(channel_id, id) ON DELETE CASCADE
|
||||
) PARTITION BY HASH (user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS channel_unread_mentions_peer_idx
|
||||
ON channel_unread_mentions (user_id, channel_id, top_message_id, message_id DESC);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
i int;
|
||||
BEGIN
|
||||
FOR i IN 0..63 LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS channel_unread_mentions_p%s PARTITION OF channel_unread_mentions FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
|
||||
lpad(i::text, 2, '0'),
|
||||
i
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE channels
|
||||
DROP COLUMN IF EXISTS participants_hidden;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE channels
|
||||
ADD COLUMN IF NOT EXISTS participants_hidden BOOLEAN NOT NULL DEFAULT false;
|
||||
9
deploy/migrations/0042_channel_colors.down.sql
Normal file
9
deploy/migrations/0042_channel_colors.down.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
ALTER TABLE channels
|
||||
DROP COLUMN IF EXISTS emoji_status_until,
|
||||
DROP COLUMN IF EXISTS emoji_status_document_id,
|
||||
DROP COLUMN IF EXISTS profile_color_background_emoji_id,
|
||||
DROP COLUMN IF EXISTS profile_color,
|
||||
DROP COLUMN IF EXISTS profile_color_set,
|
||||
DROP COLUMN IF EXISTS color_background_emoji_id,
|
||||
DROP COLUMN IF EXISTS color,
|
||||
DROP COLUMN IF EXISTS color_set;
|
||||
9
deploy/migrations/0042_channel_colors.up.sql
Normal file
9
deploy/migrations/0042_channel_colors.up.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
ALTER TABLE channels
|
||||
ADD COLUMN IF NOT EXISTS color_set BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS color INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS color_background_emoji_id BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS profile_color_set BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS profile_color INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS profile_color_background_emoji_id BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS emoji_status_document_id BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS emoji_status_until INT NOT NULL DEFAULT 0;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE channel_dialogs
|
||||
DROP COLUMN IF EXISTS view_forum_as_messages;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE channel_dialogs
|
||||
ADD COLUMN IF NOT EXISTS view_forum_as_messages BOOLEAN NOT NULL DEFAULT false;
|
||||
40
deploy/migrations/0044_channel_antispam.down.sql
Normal file
40
deploy/migrations/0044_channel_antispam.down.sql
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
-- 0044_channel_antispam rollback.
|
||||
|
||||
UPDATE app_configs
|
||||
SET hash = 3,
|
||||
config_json = config_json
|
||||
- 'telegram_antispam_group_size_min'
|
||||
- 'telegram_antispam_user_id',
|
||||
updated_at = now()
|
||||
WHERE client = 'tdesktop' AND hash = 4;
|
||||
|
||||
ALTER TABLE channel_admin_log_events
|
||||
DROP CONSTRAINT IF EXISTS channel_admin_log_events_type_check;
|
||||
|
||||
ALTER TABLE channel_admin_log_events
|
||||
ADD CONSTRAINT channel_admin_log_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'change_title',
|
||||
'change_username',
|
||||
'change_linked_chat',
|
||||
'toggle_signatures',
|
||||
'toggle_pre_history_hidden',
|
||||
'toggle_slow_mode',
|
||||
'participant_invite',
|
||||
'participant_join',
|
||||
'participant_leave',
|
||||
'participant_promote',
|
||||
'participant_demote',
|
||||
'participant_ban',
|
||||
'participant_unban',
|
||||
'participant_kick',
|
||||
'participant_unkick',
|
||||
'update_pinned',
|
||||
'send_message',
|
||||
'edit_message',
|
||||
'delete_message'
|
||||
)
|
||||
);
|
||||
|
||||
ALTER TABLE channels
|
||||
DROP COLUMN IF EXISTS antispam;
|
||||
44
deploy/migrations/0044_channel_antispam.up.sql
Normal file
44
deploy/migrations/0044_channel_antispam.up.sql
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
-- 0044_channel_antispam: persist native antispam state and expose TDesktop config.
|
||||
|
||||
ALTER TABLE channels
|
||||
ADD COLUMN IF NOT EXISTS antispam BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE channel_admin_log_events
|
||||
DROP CONSTRAINT IF EXISTS channel_admin_log_events_type_check;
|
||||
|
||||
ALTER TABLE channel_admin_log_events
|
||||
ADD CONSTRAINT channel_admin_log_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'change_title',
|
||||
'change_username',
|
||||
'change_linked_chat',
|
||||
'toggle_signatures',
|
||||
'toggle_pre_history_hidden',
|
||||
'toggle_forum',
|
||||
'toggle_anti_spam',
|
||||
'toggle_slow_mode',
|
||||
'participant_invite',
|
||||
'participant_join',
|
||||
'participant_leave',
|
||||
'participant_promote',
|
||||
'participant_demote',
|
||||
'participant_ban',
|
||||
'participant_unban',
|
||||
'participant_kick',
|
||||
'participant_unkick',
|
||||
'update_pinned',
|
||||
'send_message',
|
||||
'edit_message',
|
||||
'delete_message'
|
||||
)
|
||||
);
|
||||
|
||||
UPDATE app_configs
|
||||
SET hash = 4,
|
||||
config_json = config_json
|
||||
|| jsonb_build_object(
|
||||
'telegram_antispam_group_size_min', 200,
|
||||
'telegram_antispam_user_id', '5434988373'
|
||||
),
|
||||
updated_at = now()
|
||||
WHERE client = 'tdesktop';
|
||||
45
deploy/migrations/0045_channel_forum_tabs.down.sql
Normal file
45
deploy/migrations/0045_channel_forum_tabs.down.sql
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
-- 0045_channel_forum_tabs rollback.
|
||||
|
||||
ALTER TABLE channel_admin_log_events
|
||||
DROP CONSTRAINT IF EXISTS channel_admin_log_events_type_check;
|
||||
|
||||
DELETE FROM channel_admin_log_events
|
||||
WHERE event_type = 'toggle_forum';
|
||||
|
||||
ALTER TABLE channel_admin_log_events
|
||||
ADD CONSTRAINT channel_admin_log_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'change_title',
|
||||
'change_username',
|
||||
'change_linked_chat',
|
||||
'toggle_signatures',
|
||||
'toggle_pre_history_hidden',
|
||||
'toggle_anti_spam',
|
||||
'toggle_slow_mode',
|
||||
'participant_invite',
|
||||
'participant_join',
|
||||
'participant_leave',
|
||||
'participant_promote',
|
||||
'participant_demote',
|
||||
'participant_ban',
|
||||
'participant_unban',
|
||||
'participant_kick',
|
||||
'participant_unkick',
|
||||
'update_pinned',
|
||||
'send_message',
|
||||
'edit_message',
|
||||
'delete_message'
|
||||
)
|
||||
);
|
||||
|
||||
ALTER TABLE channels
|
||||
DROP CONSTRAINT IF EXISTS channels_kind_check;
|
||||
|
||||
ALTER TABLE channels
|
||||
ADD CONSTRAINT channels_kind_check CHECK (
|
||||
(broadcast AND NOT megagroup AND NOT forum)
|
||||
OR (megagroup AND NOT broadcast)
|
||||
);
|
||||
|
||||
ALTER TABLE channels
|
||||
DROP COLUMN IF EXISTS forum_tabs;
|
||||
48
deploy/migrations/0045_channel_forum_tabs.up.sql
Normal file
48
deploy/migrations/0045_channel_forum_tabs.up.sql
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
-- 0045_channel_forum_tabs: persist Layer 225 forum layout and admin-log action.
|
||||
|
||||
ALTER TABLE channels
|
||||
ADD COLUMN IF NOT EXISTS forum_tabs BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
UPDATE channels
|
||||
SET forum_tabs = false
|
||||
WHERE NOT forum AND forum_tabs;
|
||||
|
||||
ALTER TABLE channels
|
||||
DROP CONSTRAINT IF EXISTS channels_kind_check;
|
||||
|
||||
ALTER TABLE channels
|
||||
ADD CONSTRAINT channels_kind_check CHECK (
|
||||
((broadcast AND NOT megagroup AND NOT forum)
|
||||
OR (megagroup AND NOT broadcast))
|
||||
AND (NOT forum_tabs OR forum)
|
||||
);
|
||||
|
||||
ALTER TABLE channel_admin_log_events
|
||||
DROP CONSTRAINT IF EXISTS channel_admin_log_events_type_check;
|
||||
|
||||
ALTER TABLE channel_admin_log_events
|
||||
ADD CONSTRAINT channel_admin_log_events_type_check CHECK (
|
||||
event_type IN (
|
||||
'change_title',
|
||||
'change_username',
|
||||
'change_linked_chat',
|
||||
'toggle_signatures',
|
||||
'toggle_pre_history_hidden',
|
||||
'toggle_forum',
|
||||
'toggle_anti_spam',
|
||||
'toggle_slow_mode',
|
||||
'participant_invite',
|
||||
'participant_join',
|
||||
'participant_leave',
|
||||
'participant_promote',
|
||||
'participant_demote',
|
||||
'participant_ban',
|
||||
'participant_unban',
|
||||
'participant_kick',
|
||||
'participant_unkick',
|
||||
'update_pinned',
|
||||
'send_message',
|
||||
'edit_message',
|
||||
'delete_message'
|
||||
)
|
||||
);
|
||||
3
deploy/migrations/0046_channel_forum_topics.down.sql
Normal file
3
deploy/migrations/0046_channel_forum_topics.down.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-- 0046_channel_forum_topics rollback.
|
||||
|
||||
DROP TABLE IF EXISTS channel_forum_topics CASCADE;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue