move gotd fork into repo. (#111)

- update to latest telegram layer
- remove some references to fields in tg.Entities that don't exist in
the schema
- originally added here:
https://github.com/beeper/td/commit/820929062a2ba0104397bc01235ab58a9cff780e
  - referenced here
-
https://github.com/mautrix/telegramgo/commit/124f0967ed195b5a380c9bd02e170ada9710dde3
-
https://github.com/mautrix/telegramgo/commit/4205047aab2e0639217148b5d125bfaab668bd8e
This commit is contained in:
Adam Van Ymeren
2025-06-27 20:03:37 -07:00
committed by GitHub
parent 0952df0244
commit 7a04f298d2
19264 changed files with 1539697 additions and 84 deletions
+72
View File
@@ -0,0 +1,72 @@
// Package config contains config service implementation for tgtest server.
package config
import (
"context"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
"go.mau.fi/mautrix-telegram/pkg/gotd/tgtest"
"go.mau.fi/mautrix-telegram/pkg/gotd/tgtest/services"
)
// Service is a Telegram config service.
type Service struct {
cfg *tg.Config
cdnCfg *tg.CDNConfig
}
// NewService creates new Service.
func NewService(cfg *tg.Config, cdnCfg *tg.CDNConfig) *Service {
return &Service{cfg: cfg, cdnCfg: cdnCfg}
}
func (c *Service) HelpGetCDNConfig(ctx context.Context, req *tg.HelpGetCDNConfigRequest) (*tg.CDNConfig, error) {
cfg := c.cdnCfg
return cfg, nil
}
func (c *Service) HelpGetConfig(ctx context.Context, dc int, req *tg.HelpGetConfigRequest) (*tg.Config, error) {
cfg := *c.cfg
cfg.ThisDC = dc
return &cfg, nil
}
// OnMessage implements tgtest.Handler.
func (c *Service) OnMessage(server *tgtest.Server, req *tgtest.Request) error {
id, err := req.Buf.PeekID()
if err != nil {
return err
}
var (
decode bin.Decoder
result bin.Encoder
)
switch id {
case tg.HelpGetCDNConfigRequestTypeID:
cfg := c.cdnCfg
decode = &tg.HelpGetCDNConfigRequest{}
result = cfg
case tg.HelpGetConfigRequestTypeID:
cfg := *c.cfg
cfg.ThisDC = req.DC
decode = &tg.HelpGetConfigRequest{}
result = &cfg
default:
return services.ErrMethodNotImplemented
}
if err := decode.Decode(req.Buf); err != nil {
return err
}
return server.SendResult(req, result)
}
// Register registers service handlers.
func (c *Service) Register(dispatcher *tgtest.Dispatcher) {
dispatcher.HandleFunc(tg.HelpGetCDNConfigRequestTypeID, c.OnMessage)
dispatcher.HandleFunc(tg.HelpGetConfigRequestTypeID, c.OnMessage)
}
+2
View File
@@ -0,0 +1,2 @@
// Package services contains some Telegram services implemented for testing.
package services
+16
View File
@@ -0,0 +1,16 @@
package services
import (
"go.mau.fi/mautrix-telegram/pkg/gotd/tgerr"
"go.mau.fi/mautrix-telegram/pkg/gotd/tgtest"
)
var (
// ErrMethodNotImplemented denotes that method is not implemented.
ErrMethodNotImplemented error = tgerr.New(400, "INPUT_METHOD_INVALID")
// NotImplemented is a simple handler which returns ErrMethodNotImplemented.
NotImplemented tgtest.HandlerFunc = func(server *tgtest.Server, req *tgtest.Request) error {
return ErrMethodNotImplemented
}
)
+24
View File
@@ -0,0 +1,24 @@
package file
type Config struct {
// Storage to store files.
// InMemory will be used.
Storage Storage
// HashPartSize is a size of part to use in tg.FileHash.
HashPartSize int
// HashRangeSize is size of range to return in upload.getFileHashes.
HashRangeSize int
}
func (c *Config) setDefaults() {
if c.Storage == nil {
c.Storage = NewInMemory()
}
// Telegram usually uses this values.
if c.HashPartSize == 0 {
c.HashPartSize = 131072
}
if c.HashRangeSize == 0 {
c.HashRangeSize = 10
}
}
+136
View File
@@ -0,0 +1,136 @@
package file
import (
"context"
"fmt"
"github.com/go-faster/errors"
"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
"go.mau.fi/mautrix-telegram/pkg/gotd/tgerr"
)
func getLocation(loc tg.InputFileLocationClass) (string, error) {
v, ok := loc.(interface {
GetLocalID() int
GetVolumeID() int64
})
if !ok {
return "", tgerr.New(400, tg.ErrFileIDInvalid)
}
return fmt.Sprintf("%d_%d", v.GetLocalID(), v.GetVolumeID()), nil
}
func (m *Service) openLocation(loc tg.InputFileLocationClass) (File, error) {
name, err := getLocation(loc)
if err != nil {
return nil, err
}
f, err := m.storage.Open(name)
if err != nil {
return nil, tgerr.New(400, tg.ErrFileIDInvalid)
}
return f, nil
}
func (m *Service) getPart(loc tg.InputFileLocationClass, offset int64, limit int) ([]byte, error) {
f, err := m.openLocation(loc)
if err != nil {
return nil, err
}
r := make([]byte, limit)
n, err := f.ReadAt(r, offset)
if err != nil {
return nil, errors.Wrap(err, "read from storage")
}
return r[:n], nil
}
func (m *Service) UploadGetFile(ctx context.Context, request *tg.UploadGetFileRequest) (tg.UploadFileClass, error) {
data, err := m.getPart(request.Location, request.Offset, request.Limit)
if err != nil {
return nil, err
}
return &tg.UploadFile{
Type: &tg.StorageFilePartial{},
Mtime: 0,
Bytes: data,
}, nil
}
func countHashes(data []byte, offset int64, partSize int) []tg.FileHash {
actions := data
batchSize := partSize
batches := make([][]byte, 0, (len(actions)+batchSize-1)/batchSize)
for batchSize < len(actions) {
actions, batches = actions[batchSize:], append(batches, actions[0:batchSize:batchSize])
}
batches = append(batches, actions)
currentRange := make([]tg.FileHash, 0, 10)
for _, batch := range batches {
currentRange = append(currentRange, tg.FileHash{
Offset: offset,
Limit: partSize,
Hash: crypto.SHA256(batch),
})
offset += int64(len(batch))
}
return currentRange
}
func divAndCeil(a, b int) int {
r := a / b
if a%b != 0 {
r++
}
return r
}
// computeBatch computes hash range number for given offset.
func computeBatch(offset int64, rangeSize, partSize int) int {
// Compute number of parts in partSize from offset.
parts := divAndCeil(int(offset+1), partSize)
// Compute number of hash ranges in rangeSize.
batches := divAndCeil(parts, rangeSize)
return batches
}
func (m *Service) UploadGetFileHashes(
ctx context.Context,
request *tg.UploadGetFileHashesRequest,
) ([]tg.FileHash, error) {
f, err := m.openLocation(request.Location)
if err != nil {
return nil, err
}
if request.Offset >= int64(f.Size()) {
return nil, nil
}
partSize := m.hashPartSize
rangeSize := m.hashRangeSize
batch := computeBatch(request.Offset, rangeSize, partSize)
low := (batch - 1) * rangeSize * partSize
high := batch * rangeSize * partSize
r := make([]byte, high-low)
n, err := f.ReadAt(r, int64(low))
if err != nil {
return nil, err
}
r = r[:n]
return countHashes(r, int64(low), partSize), nil
}
+97
View File
@@ -0,0 +1,97 @@
// Package file contains file service implementation for tgtest server.
package file
import (
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
"go.mau.fi/mautrix-telegram/pkg/gotd/tgtest"
"go.mau.fi/mautrix-telegram/pkg/gotd/tgtest/services"
)
// Service is a Telegram file service.
type Service struct {
storage Storage
// Size of part to use in tg.FileHash.
hashPartSize int
// Size of range to return in upload.getFileHashes.
hashRangeSize int
}
// NewService creates new file Service.
func NewService(cfg Config) *Service {
cfg.setDefaults()
return &Service{
storage: cfg.Storage,
hashPartSize: cfg.HashPartSize,
hashRangeSize: cfg.HashRangeSize,
}
}
// OnMessage implements tgtest.Handler.
func (m *Service) OnMessage(server *tgtest.Server, req *tgtest.Request) error {
id, err := req.Buf.PeekID()
if err != nil {
return err
}
switch id {
case tg.UploadGetFileRequestTypeID:
fileReq := tg.UploadGetFileRequest{}
if err := fileReq.Decode(req.Buf); err != nil {
return err
}
r, err := m.UploadGetFile(req.RequestCtx, &fileReq)
if err != nil {
return err
}
return server.SendResult(req, r)
case tg.UploadGetFileHashesRequestTypeID:
fileReq := tg.UploadGetFileHashesRequest{}
if err := fileReq.Decode(req.Buf); err != nil {
return err
}
r, err := m.UploadGetFileHashes(req.RequestCtx, &fileReq)
if err != nil {
return err
}
return server.SendResult(req, &tg.FileHashVector{Elems: r})
case tg.UploadSaveFilePartRequestTypeID:
fileReq := tg.UploadSaveFilePartRequest{}
if err := fileReq.Decode(req.Buf); err != nil {
return err
}
r, err := m.UploadSaveFilePart(req.RequestCtx, &fileReq)
if err != nil {
return err
}
return server.SendBool(req, r)
case tg.UploadSaveBigFilePartRequestTypeID:
fileReq := tg.UploadSaveBigFilePartRequest{}
if err := fileReq.Decode(req.Buf); err != nil {
return err
}
r, err := m.UploadSaveBigFilePart(req.RequestCtx, &fileReq)
if err != nil {
return err
}
return server.SendBool(req, r)
default:
return services.ErrMethodNotImplemented
}
}
// Register registers service handlers.
func (m *Service) Register(dispatcher *tgtest.Dispatcher) {
dispatcher.HandleFunc(tg.UploadGetFileRequestTypeID, m.OnMessage)
dispatcher.HandleFunc(tg.UploadGetFileHashesRequestTypeID, m.OnMessage)
dispatcher.HandleFunc(tg.UploadSaveFilePartRequestTypeID, m.OnMessage)
dispatcher.HandleFunc(tg.UploadSaveBigFilePartRequestTypeID, m.OnMessage)
}
+72
View File
@@ -0,0 +1,72 @@
package file
import (
"io"
"sync"
"sync/atomic"
"go.mau.fi/mautrix-telegram/pkg/gotd/syncio"
)
// File represents Telegram file.
type File interface {
io.ReaderAt
io.WriterAt
io.Closer
PartSize() int
SetPartSize(v int)
Size() int
}
// Storage is an abstraction for Telegram file storage.
type Storage interface {
Open(name string) (File, error)
}
type memFile struct {
syncio.BufWriterAt
partSize int32
_ [4]byte
}
func (m *memFile) Size() int {
return m.Len()
}
func (m *memFile) Close() error {
return nil
}
func (m *memFile) PartSize() int {
return int(atomic.LoadInt32(&m.partSize))
}
func (m *memFile) SetPartSize(v int) {
atomic.StoreInt32(&m.partSize, int32(v))
}
// InMemory is an inmemory implementation of file storage.
type InMemory struct {
files map[string]*memFile
filesMux sync.Mutex
}
// NewInMemory creates new InMemory.
func NewInMemory() *InMemory {
return &InMemory{
files: map[string]*memFile{},
}
}
// Open implement Storage.
func (i *InMemory) Open(name string) (File, error) {
i.filesMux.Lock()
defer i.filesMux.Unlock()
file, ok := i.files[name]
if !ok {
file = &memFile{}
i.files[name] = file
}
return file, nil
}
+115
View File
@@ -0,0 +1,115 @@
package file
import (
"context"
"fmt"
"github.com/go-faster/errors"
"go.uber.org/multierr"
"go.mau.fi/mautrix-telegram/pkg/gotd/constant"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
"go.mau.fi/mautrix-telegram/pkg/gotd/tgerr"
)
// https://core.telegram.org/api/files#uploading-files
const (
// Each part should have a sequence number, file_part, with a value ranging from 0 to 3,999.
uploadPartsLimit = constant.UploadMaxParts
// `part_size % 1024 = 0` (divisible by 1KB)
uploadPaddingPartSize = constant.UploadPadding
// `524288 % part_size = 0` (512KB must be evenly divisible by part_size)
uploadMaximumPartSize = constant.UploadMaxPartSize
)
type upload interface {
// GetFileID returns random file identifier created by the client.
GetFileID() int64
// GetFilePart returns numerical order of a part.
GetFilePart() int
// GetBytes returns binary data, content of a part.
GetBytes() []byte
}
func validatePartSize(got, stored int) *tgerr.Error {
switch {
case got == 0:
return tgerr.New(400, tg.ErrFilePartEmpty)
case got > uploadMaximumPartSize:
return tgerr.New(400, tg.ErrFilePartTooBig)
}
if stored == 0 {
return nil
}
switch {
case got != stored:
return tgerr.New(400, tg.ErrFilePartSizeChanged)
case uploadMaximumPartSize%got != 0,
got%uploadPaddingPartSize != 0:
return tgerr.New(400, tg.ErrFilePartSizeInvalid)
default:
return nil
}
}
func (m *Service) write(ctx context.Context, request upload) (err error) {
// TODO(tdakkota): Better way to handle user id. For now we haven't auth service to pair
// user ID and authkey
id, ok := ctx.Value("user_id").(int)
if !ok {
id = 10
}
file, err := m.storage.Open(fmt.Sprintf("%d_%d", id, request.GetFileID()))
if err != nil {
return errors.Wrap(err, "open file")
}
defer func() {
multierr.AppendInto(&err, file.Close())
}()
part := request.GetFilePart()
if part < 0 || part > uploadPartsLimit {
return tgerr.New(400, tg.ErrFilePartInvalid)
}
data := request.GetBytes()
partSize := file.PartSize()
if err := validatePartSize(len(data), partSize); err != nil {
return err
}
if partSize == 0 {
partSize = len(data)
file.SetPartSize(partSize)
}
offset := int64(partSize * part)
if _, err := file.WriteAt(data, offset); err != nil {
return errors.Errorf("write at %d-%d", offset, offset+int64(len(data)))
}
return nil
}
func (m *Service) UploadSaveFilePart(ctx context.Context, request *tg.UploadSaveFilePartRequest) (bool, error) {
if err := m.write(ctx, request); err != nil {
return false, err
}
return true, nil
}
func (m *Service) UploadSaveBigFilePart(ctx context.Context, request *tg.UploadSaveBigFilePartRequest) (bool, error) {
part := request.FileTotalParts
if part < 0 || part > uploadPartsLimit {
return false, tgerr.New(400, tg.ErrFilePartsInvalid)
}
if err := m.write(ctx, request); err != nil {
return false, err
}
return true, nil
}