├── .github ├── codeql │ └── config.yml ├── release.yml ├── workflows │ ├── commitlint.yml │ ├── codeql.yaml │ ├── ci.yml │ ├── coverage.yml │ ├── lint.yml │ └── e2e.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.md │ └── bug-report.md └── stale.yml ├── auth ├── dialog │ ├── doc.go │ └── dialog.go ├── kv │ ├── doc.go │ ├── kv.go │ ├── session.go │ └── auth.go ├── terminal │ ├── doc.go │ ├── code_test.go │ └── code.go ├── localization │ ├── doc.go │ └── catalog.go ├── doc.go ├── credentials.go ├── build.go ├── ask.go ├── signup_test.go ├── build_test.go └── signup.go ├── invoker ├── doc.go ├── update.go ├── debug.go └── update_test.go ├── redis ├── doc.go ├── auth.go ├── session.go ├── client.go ├── e2e_test.go ├── auth_example_test.go ├── session_example_test.go └── peer_storage.go ├── s3 ├── doc.go ├── e2e_test.go ├── session_example_test.go └── session.go ├── bbolt ├── doc.go ├── auth.go ├── session.go ├── state_storage_test.go ├── e2e_test.go ├── client.go ├── session_example_test.go ├── auth_example_test.go ├── peer_storage.go └── state_storage.go ├── middleware ├── doc.go ├── ratelimit │ ├── doc.go │ └── ratelimit.go ├── floodwait │ ├── doc.go │ ├── request.go │ ├── queue_test.go │ ├── scheduler_test.go │ ├── scheduler.go │ ├── queue.go │ ├── simple_waiter.go │ └── waiter.go ├── tg_prom │ ├── middleware_test.go │ └── middleware.go └── example_test.go ├── go.test.sh ├── internal └── tests │ ├── doc.go │ ├── backoff.go │ └── storage.go ├── pebble ├── doc.go ├── auth.go ├── session.go ├── e2e_test.go ├── client.go ├── session_example_test.go ├── auth_example_test.go └── peer_storage.go ├── storage ├── doc.go ├── find.go ├── peer_storage.go ├── hook.go ├── peer_as.go ├── collector_example_test.go ├── key_test.go ├── resolver_cache_example_test.go ├── find_test.go ├── hook_test.go ├── hook_example_test.go ├── resolver_cache.go ├── key.go ├── collector.go ├── peer_test.go ├── collector_test.go ├── resolver_cache_test.go └── peer.go ├── _tools ├── go.mod ├── tools.go └── go.sum ├── vault ├── doc.go ├── auth.go ├── session.go ├── session_example_test.go ├── e2e_test.go ├── auth_example_test.go └── client.go ├── go.e2e.sh ├── go.coverage.sh ├── .codecov.yaml ├── .gitignore ├── clock ├── ntp_example_test.go └── ntp.go ├── README.md ├── Makefile ├── bg ├── connect_test.go └── connect.go ├── LICENSE ├── oteltg ├── middleware_test.go └── middleware.go ├── http_range ├── LICENSE ├── range_test.go └── range.go ├── tg_io ├── download.go └── download_test.go ├── .golangci.yml ├── http_io └── io.go ├── partio ├── streamer_test.go └── streamer.go └── go.mod /.github/codeql/config.yml: -------------------------------------------------------------------------------- 1 | paths: 2 | - 'auth' 3 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | -------------------------------------------------------------------------------- /auth/dialog/doc.go: -------------------------------------------------------------------------------- 1 | // Package dialog contains GUI dialog Telegram authenticator. 2 | package dialog 3 | -------------------------------------------------------------------------------- /invoker/doc.go: -------------------------------------------------------------------------------- 1 | // Package invoker contains RPC invoker helpers and middlewares. 2 | package invoker 3 | -------------------------------------------------------------------------------- /redis/doc.go: -------------------------------------------------------------------------------- 1 | // Package redis contains gotd storage implementations using Redis. 2 | package redis 3 | -------------------------------------------------------------------------------- /s3/doc.go: -------------------------------------------------------------------------------- 1 | // Package s3 contains gotd storage implementations using S3 protocol. 2 | package s3 3 | -------------------------------------------------------------------------------- /auth/kv/doc.go: -------------------------------------------------------------------------------- 1 | // Package kv contains wrapper implementations over generic KV storage. 2 | package kv 3 | -------------------------------------------------------------------------------- /bbolt/doc.go: -------------------------------------------------------------------------------- 1 | // Package bbolt contains gotd storage implementations using etcd bbolt. 2 | package bbolt 3 | -------------------------------------------------------------------------------- /middleware/doc.go: -------------------------------------------------------------------------------- 1 | // Package middleware wraps some useful middlewares for telegram. 2 | package middleware 3 | -------------------------------------------------------------------------------- /go.test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # test with -race 6 | go test --timeout 5m -race ./... 7 | -------------------------------------------------------------------------------- /internal/tests/doc.go: -------------------------------------------------------------------------------- 1 | // Package tests contains common storage tests and some test utilities. 2 | package tests 3 | -------------------------------------------------------------------------------- /pebble/doc.go: -------------------------------------------------------------------------------- 1 | // Package pebble contains gotd storage implementations using CockroachDB pebble. 2 | package pebble 3 | -------------------------------------------------------------------------------- /storage/doc.go: -------------------------------------------------------------------------------- 1 | // Package storage contains common structures for iterating over peer storage. 2 | package storage 3 | -------------------------------------------------------------------------------- /_tools/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gotd/contrib/_tools 2 | 3 | go 1.16 4 | 5 | require github.com/golang/mock v1.5.0 6 | -------------------------------------------------------------------------------- /vault/doc.go: -------------------------------------------------------------------------------- 1 | // Package vault contains gotd secret storage implementations using Hashicorp Vault. 2 | package vault 3 | -------------------------------------------------------------------------------- /auth/terminal/doc.go: -------------------------------------------------------------------------------- 1 | // Package terminal contains authenticator implementation 2 | // using terminal. 3 | package terminal 4 | -------------------------------------------------------------------------------- /middleware/ratelimit/doc.go: -------------------------------------------------------------------------------- 1 | // Package ratelimit implements a tg.Invoker that limits request rate. 2 | package ratelimit 3 | -------------------------------------------------------------------------------- /middleware/floodwait/doc.go: -------------------------------------------------------------------------------- 1 | // Package floodwait implements a tg.Invoker that handles flood wait errors. 2 | package floodwait 3 | -------------------------------------------------------------------------------- /_tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | _ "github.com/golang/mock/mockgen" 8 | ) 9 | -------------------------------------------------------------------------------- /auth/localization/doc.go: -------------------------------------------------------------------------------- 1 | // Package localization contains localization helpers for terminal and dialog authenticator. 2 | package localization 3 | -------------------------------------------------------------------------------- /go.e2e.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | go test -v -coverpkg=$1 -coverprofile=profile.out $1 6 | go tool cover -func profile.out 7 | -------------------------------------------------------------------------------- /auth/doc.go: -------------------------------------------------------------------------------- 1 | // Package auth provides some interfaces, implementations 2 | // and utility function for telegram.UserAuthenticator. 3 | package auth 4 | -------------------------------------------------------------------------------- /go.coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | go test -v -coverpkg=./... -coverprofile=profile.out ./... 6 | go tool cover -func profile.out 7 | -------------------------------------------------------------------------------- /auth/credentials.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "context" 4 | 5 | // Credentials represents Telegram user credentials. 6 | type Credentials interface { 7 | Phone(ctx context.Context) (string, error) 8 | Password(ctx context.Context) (string, error) 9 | } 10 | -------------------------------------------------------------------------------- /.codecov.yaml: -------------------------------------------------------------------------------- 1 | ignore: 2 | # Ignore generated files. 3 | - "**/oas_*_gen.go" 4 | # Ignore commands (examples or internal utilities). 5 | - "cmd/*/*.go" 6 | - "**/cmd/*/*.go" 7 | # Ignore examples 8 | - "examples/**" 9 | 10 | coverage: 11 | status: 12 | project: false 13 | patch: false 14 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Commit Messages 2 | on: [pull_request] 3 | 4 | jobs: 5 | commitlint: 6 | runs-on: ubuntu-latest 7 | if: github.actor != 'dependabot[bot]' 8 | steps: 9 | - uses: actions/checkout@v5.0.0 10 | with: 11 | fetch-depth: 0 12 | - uses: wagoid/commitlint-github-action@v6.2.1 13 | -------------------------------------------------------------------------------- /middleware/tg_prom/middleware_test.go: -------------------------------------------------------------------------------- 1 | package tg_prom 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestPrometheus(t *testing.T) { 11 | r := prometheus.NewPedanticRegistry() 12 | p := New() 13 | 14 | for _, m := range p.Metrics() { 15 | require.NoError(t, r.Register(m)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /auth/build.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | tgauth "github.com/gotd/td/telegram/auth" 5 | ) 6 | 7 | type auth struct { 8 | Credentials 9 | Ask 10 | } 11 | 12 | var _ tgauth.UserAuthenticator = auth{} 13 | 14 | // Build creates new UserAuthenticator. 15 | func Build(cred Credentials, ask Ask) tgauth.UserAuthenticator { 16 | return auth{ 17 | Credentials: cred, 18 | Ask: ask, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | *.iml 4 | out 5 | gen 6 | ### Go template 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, built with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | # vendor/ 22 | 23 | -------------------------------------------------------------------------------- /auth/kv/kv.go: -------------------------------------------------------------------------------- 1 | package kv 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | ) 8 | 9 | // Storage represents generic KV storage. 10 | type Storage interface { 11 | Set(ctx context.Context, k, v string) error 12 | Get(ctx context.Context, k string) (string, error) 13 | } 14 | 15 | // ErrKeyNotFound is a special error to return when given key not found. 16 | var ErrKeyNotFound = errors.New("key not found") 17 | -------------------------------------------------------------------------------- /redis/auth.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/go-redis/redis/v8" 5 | 6 | "github.com/gotd/contrib/auth/kv" 7 | ) 8 | 9 | // Credentials stores user credentials to Redis. 10 | type Credentials struct { 11 | kv.Credentials 12 | } 13 | 14 | // NewCredentials creates new Credentials. 15 | func NewCredentials(client *redis.Client) Credentials { 16 | s := redisClient{ 17 | client: client, 18 | } 19 | return Credentials{kv.NewCredentials(s)} 20 | } 21 | -------------------------------------------------------------------------------- /bbolt/auth.go: -------------------------------------------------------------------------------- 1 | package bbolt 2 | 3 | import ( 4 | "go.etcd.io/bbolt" 5 | 6 | "github.com/gotd/contrib/auth/kv" 7 | ) 8 | 9 | // Credentials stores user credentials to bbolt. 10 | type Credentials struct { 11 | kv.Credentials 12 | } 13 | 14 | // NewCredentials creates new Credentials. 15 | func NewCredentials(db *bbolt.DB, bucket []byte) Credentials { 16 | s := bboltStorage{db: db, bucket: bucket} 17 | return Credentials{ 18 | Credentials: kv.NewCredentials(s), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /clock/ntp_example_test.go: -------------------------------------------------------------------------------- 1 | package clock_test 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/gotd/contrib/clock" 7 | "github.com/gotd/td/telegram" 8 | ) 9 | 10 | func ExampleNewNTP() { 11 | c, err := clock.NewNTP() // or clock.NewNTP("my.ntp.host") 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | 16 | client, err := telegram.ClientFromEnvironment(telegram.Options{ 17 | Clock: c, 18 | }) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | _ = client 24 | } 25 | -------------------------------------------------------------------------------- /pebble/auth.go: -------------------------------------------------------------------------------- 1 | package pebble 2 | 3 | import ( 4 | "github.com/cockroachdb/pebble" 5 | 6 | "github.com/gotd/contrib/auth/kv" 7 | ) 8 | 9 | // Credentials stores user credentials to Pebble. 10 | type Credentials struct { 11 | kv.Credentials 12 | } 13 | 14 | // NewCredentials creates new Credentials. 15 | func NewCredentials(db *pebble.DB) Credentials { 16 | s := pebbleStorage{db: db, opts: pebble.Sync} 17 | return Credentials{ 18 | Credentials: kv.NewCredentials(s), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /vault/auth.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "github.com/hashicorp/vault/api" 5 | 6 | "github.com/gotd/contrib/auth/kv" 7 | ) 8 | 9 | // Credentials stores user credentials to Vault. 10 | type Credentials struct { 11 | kv.Credentials 12 | } 13 | 14 | // NewCredentials creates new Credentials. 15 | func NewCredentials(client *api.Client, path string) Credentials { 16 | s := vaultClient{client: client, path: path} 17 | return Credentials{ 18 | Credentials: kv.NewCredentials(s), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | groups: 8 | opentelemetry: 9 | patterns: 10 | - "go.opentelemetry.io/otel" 11 | - "go.opentelemetry.io/otel/*" 12 | - "go.opentelemetry.io/contrib/*" 13 | golang: 14 | patterns: 15 | - "golang.org/x/*" 16 | - package-ecosystem: github-actions 17 | directory: "/" 18 | schedule: 19 | interval: daily 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # contrib 2 | 3 | [![Go](https://github.com/gotd/contrib/workflows/CI/badge.svg)](https://github.com/gotd/contrib/actions) 4 | [![Documentation](https://godoc.org/github.com/gotd/contrib?status.svg)](https://pkg.go.dev/github.com/gotd/contrib) 5 | [![license](https://img.shields.io/github/license/gotd/contrib.svg?maxAge=2592000)](https://github.com/gotd/contrib/blob/master/LICENSE) 6 | 7 | Contrib package for [gotd](https://github.com/gotd/td). 8 | 9 | ## Install 10 | ``` 11 | go get github.com/gotd/contrib 12 | ``` 13 | -------------------------------------------------------------------------------- /auth/ask.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | tgauth "github.com/gotd/td/telegram/auth" 5 | ) 6 | 7 | // Ask represents parts of auth flow which 8 | // requires user interaction. 9 | type Ask interface { 10 | tgauth.CodeAuthenticator 11 | SignUpFlow 12 | } 13 | 14 | type ask struct { 15 | tgauth.CodeAuthenticator 16 | SignUpFlow 17 | } 18 | 19 | var _ Ask = ask{} 20 | 21 | // BuildAsk creates new Ask. 22 | func BuildAsk(code tgauth.CodeAuthenticator, signUp SignUpFlow) Ask { 23 | return ask{ 24 | CodeAuthenticator: code, 25 | SignUpFlow: signUp, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @./go.test.sh 3 | .PHONY: test 4 | 5 | e2e_redis_test: 6 | @./go.e2e.sh ./redis/... 7 | .PHONY: e2e_redis_test 8 | 9 | e2e_vault_test: 10 | @./go.e2e.sh ./vault/... 11 | .PHONY: e2e_vault_test 12 | 13 | e2e_etcd_test: 14 | @./go.e2e.sh ./etcd/... 15 | .PHONY: e2e_etcd_test 16 | 17 | e2e_s3_test: 18 | @./go.e2e.sh ./s3/... 19 | .PHONY: e2e_s3_test 20 | 21 | e2e_tg_io_test: 22 | @./go.e2e.sh ./tg_io/... 23 | .PHONY: e2e_tg_io 24 | 25 | coverage: 26 | @./go.coverage.sh 27 | .PHONY: coverage 28 | 29 | generate: 30 | go generate ./... 31 | .PHONY: generate 32 | -------------------------------------------------------------------------------- /redis/session.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/go-redis/redis/v8" 5 | 6 | "github.com/gotd/td/session" 7 | 8 | "github.com/gotd/contrib/auth/kv" 9 | ) 10 | 11 | var _ session.Storage = SessionStorage{} 12 | 13 | // SessionStorage is a MTProto session Redis storage. 14 | type SessionStorage struct { 15 | kv.Session 16 | } 17 | 18 | // NewSessionStorage creates new SessionStorage. 19 | func NewSessionStorage(client *redis.Client, key string) SessionStorage { 20 | s := redisClient{client: client} 21 | return SessionStorage{ 22 | Session: kv.NewSession(s, key), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /bbolt/session.go: -------------------------------------------------------------------------------- 1 | package bbolt 2 | 3 | import ( 4 | "go.etcd.io/bbolt" 5 | 6 | "github.com/gotd/td/session" 7 | 8 | "github.com/gotd/contrib/auth/kv" 9 | ) 10 | 11 | var _ session.Storage = SessionStorage{} 12 | 13 | // SessionStorage is a MTProto session bbolt storage. 14 | type SessionStorage struct { 15 | kv.Session 16 | } 17 | 18 | // NewSessionStorage creates new SessionStorage. 19 | func NewSessionStorage(db *bbolt.DB, key string, bucket []byte) SessionStorage { 20 | s := bboltStorage{db: db, bucket: bucket} 21 | return SessionStorage{ 22 | Session: kv.NewSession(s, key), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pebble/session.go: -------------------------------------------------------------------------------- 1 | package pebble 2 | 3 | import ( 4 | "github.com/cockroachdb/pebble" 5 | 6 | "github.com/gotd/td/session" 7 | 8 | "github.com/gotd/contrib/auth/kv" 9 | ) 10 | 11 | var _ session.Storage = SessionStorage{} 12 | 13 | // SessionStorage is a MTProto session Pebble storage. 14 | type SessionStorage struct { 15 | kv.Session 16 | } 17 | 18 | // NewSessionStorage creates new SessionStorage. 19 | func NewSessionStorage(db *pebble.DB, key string) SessionStorage { 20 | s := pebbleStorage{db: db, opts: pebble.Sync} 21 | return SessionStorage{ 22 | Session: kv.NewSession(s, key), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /vault/session.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "github.com/hashicorp/vault/api" 5 | 6 | "github.com/gotd/td/session" 7 | 8 | "github.com/gotd/contrib/auth/kv" 9 | ) 10 | 11 | var _ session.Storage = SessionStorage{} 12 | 13 | // SessionStorage is a MTProto session Vault storage. 14 | type SessionStorage struct { 15 | kv.Session 16 | } 17 | 18 | // NewSessionStorage creates new SessionStorage. 19 | func NewSessionStorage(client *api.Client, path, key string) SessionStorage { 20 | s := vaultClient{client: client, path: path} 21 | return SessionStorage{ 22 | Session: kv.NewSession(s, key), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Ask on Telegram (Russian) 3 | url: https://t.me/gotd_ru 4 | about: You can ask for help in Russian here! 5 | - name: Ask on Telegram (English) 6 | url: https://t.me/gotd_en 7 | about: You can ask for help in English here! 8 | - name: Ask on GitHub 9 | url: https://github.com/gotd/td/discussions/new?category=q-a 10 | about: You can ask for help without using Telegram too! 11 | - name: Want to contribute to gotd? 12 | url: https://github.com/gotd/td/blob/main/CONTRIBUTING.md 13 | about: Be sure to read contributing guidelines! 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Use this template for proposing features. 4 | title: "[SUBJECT]: [DESCRIPTION]" 5 | labels: enhancement 6 | --- 7 | 8 | 9 | 10 | ### Description 11 | 12 | 16 | 17 | ### References 18 | 19 | 22 | -------------------------------------------------------------------------------- /bbolt/state_storage_test.go: -------------------------------------------------------------------------------- 1 | package bbolt 2 | 3 | import ( 4 | "context" 5 | "path" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | bboltdb "go.etcd.io/bbolt" 10 | ) 11 | 12 | func TestState(t *testing.T) { 13 | db, err := bboltdb.Open(path.Join(t.TempDir(), "bbolt.db"), 0666, &bboltdb.Options{}) // nolint:gocritic 14 | require.NoError(t, err) 15 | 16 | state := NewStateStorage(db) 17 | 18 | ctx := context.Background() 19 | cb := func(ctx context.Context, channelID int64, pts int) error { 20 | return nil 21 | } 22 | require.NoError(t, state.ForEachChannels(ctx, 0, cb)) 23 | require.NoError(t, db.Close()) 24 | } 25 | -------------------------------------------------------------------------------- /pebble/e2e_test.go: -------------------------------------------------------------------------------- 1 | package pebble_test 2 | 3 | import ( 4 | "testing" 5 | 6 | pebbledb "github.com/cockroachdb/pebble" 7 | "github.com/cockroachdb/pebble/vfs" 8 | 9 | "github.com/gotd/contrib/internal/tests" 10 | "github.com/gotd/contrib/pebble" 11 | ) 12 | 13 | func TestE2E(t *testing.T) { 14 | db, err := pebbledb.Open("pebble.db", &pebbledb.Options{ 15 | FS: vfs.NewMem(), 16 | }) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | tests.TestSessionStorage(t, pebble.NewSessionStorage(db, "testsession")) 22 | tests.TestCredentials(t, pebble.NewCredentials(db)) 23 | tests.TestPeerStorage(t, pebble.NewPeerStorage(db)) 24 | } 25 | -------------------------------------------------------------------------------- /middleware/floodwait/request.go: -------------------------------------------------------------------------------- 1 | package floodwait 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gotd/td/bin" 7 | "github.com/gotd/td/tg" 8 | ) 9 | 10 | // object is a abstraction for Telegram API object with TypeID. 11 | type object interface { 12 | TypeID() uint32 13 | } 14 | 15 | type key uint64 16 | 17 | func (k *key) fromEncoder(encoder bin.Encoder) { 18 | obj, ok := encoder.(object) 19 | if !ok { 20 | return 21 | } 22 | *k = key(obj.TypeID()) 23 | } 24 | 25 | type request struct { 26 | ctx context.Context 27 | input bin.Encoder 28 | output bin.Decoder 29 | next tg.Invoker 30 | key key 31 | 32 | retry int 33 | result chan error 34 | } 35 | -------------------------------------------------------------------------------- /redis/client.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | "github.com/go-redis/redis/v8" 8 | 9 | "github.com/gotd/contrib/auth/kv" 10 | ) 11 | 12 | type redisClient struct { 13 | client *redis.Client 14 | } 15 | 16 | func (r redisClient) Set(ctx context.Context, k, v string) error { 17 | return r.client.Set(ctx, k, v, 0).Err() 18 | } 19 | 20 | func (r redisClient) Get(ctx context.Context, k string) (string, error) { 21 | v, err := r.client.Get(ctx, k).Result() 22 | if err != nil { 23 | if errors.Is(err, redis.Nil) { 24 | return "", kv.ErrKeyNotFound 25 | } 26 | return "", err 27 | } 28 | 29 | return v, nil 30 | } 31 | -------------------------------------------------------------------------------- /bg/connect_test.go: -------------------------------------------------------------------------------- 1 | package bg 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | type testKey string 12 | 13 | type testClient struct { 14 | tt *testing.T 15 | } 16 | 17 | func (t testClient) Run(ctx context.Context, f func(ctx context.Context) error) error { 18 | assert.Equal(t.tt, "bar", ctx.Value(testKey("foo"))) 19 | return f(ctx) 20 | } 21 | 22 | func TestConnect(t *testing.T) { 23 | ctx := context.Background() 24 | ctx = context.WithValue(ctx, testKey("foo"), "bar") 25 | stop, err := Connect(testClient{tt: t}, WithContext(ctx)) 26 | require.NoError(t, err) 27 | require.NoError(t, stop()) 28 | } 29 | -------------------------------------------------------------------------------- /bbolt/e2e_test.go: -------------------------------------------------------------------------------- 1 | package bbolt_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | bboltdb "go.etcd.io/bbolt" 8 | 9 | "github.com/gotd/contrib/bbolt" 10 | "github.com/gotd/contrib/internal/tests" 11 | ) 12 | 13 | func TestE2E(t *testing.T) { 14 | db, err := bboltdb.Open("bbolt.db", 0, &bboltdb.Options{ 15 | OpenFile: func(s string, flag int, mode os.FileMode) (*os.File, error) { 16 | return os.CreateTemp("", "*"+s) 17 | }, 18 | }) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | bucket := []byte("test") 23 | 24 | tests.TestSessionStorage(t, bbolt.NewSessionStorage(db, "testsession", bucket)) 25 | tests.TestCredentials(t, bbolt.NewCredentials(db, bucket)) 26 | tests.TestPeerStorage(t, bbolt.NewPeerStorage(db, bucket)) 27 | } 28 | -------------------------------------------------------------------------------- /middleware/floodwait/queue_test.go: -------------------------------------------------------------------------------- 1 | package floodwait 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestQueue(t *testing.T) { 11 | a := require.New(t) 12 | 13 | q := newQueue(16) 14 | now := time.Date(2077, 10, 23, 0, 3, 0, 0, time.UTC) 15 | for i := range [10]struct{}{} { 16 | q.add(request{ 17 | key: key(i), 18 | }, now.Add(time.Duration(i)*time.Second)) 19 | } 20 | 21 | a.Equal(10, q.len()) 22 | 23 | a.Len(q.gather(now.Add(1*time.Millisecond), nil), 1) 24 | a.Equal(9, q.len()) 25 | 26 | now = now.Add(10 * time.Second) 27 | q.move(5, now, 10*time.Second) 28 | 29 | a.Len(q.gather(now, nil), 8) 30 | a.Equal(1, q.len()) 31 | 32 | a.Len(q.gather(now.Add(10*time.Second), nil), 1) 33 | a.Equal(0, q.len()) 34 | } 35 | -------------------------------------------------------------------------------- /redis/e2e_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | redisclient "github.com/go-redis/redis/v8" 9 | 10 | "github.com/gotd/contrib/internal/tests" 11 | "github.com/gotd/contrib/redis" 12 | ) 13 | 14 | func TestE2E(t *testing.T) { 15 | addr := os.Getenv("REDIS_ADDR") 16 | if addr == "" { 17 | t.Skip("Set REDIS_ADDR to run E2E test") 18 | } 19 | 20 | client := redisclient.NewClient(&redisclient.Options{ 21 | Addr: addr, 22 | }) 23 | tests.RetryUntilAvailable(t, "Redis", addr, func(ctx context.Context) error { 24 | return client.Ping(ctx).Err() 25 | }) 26 | 27 | tests.TestSessionStorage(t, redis.NewSessionStorage(client, "session")) 28 | tests.TestCredentials(t, redis.NewCredentials(client)) 29 | tests.TestPeerStorage(t, redis.NewPeerStorage(client)) 30 | } 31 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 366 3 | 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 30 6 | 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - pinned 10 | - security 11 | - bug 12 | - blocked 13 | - protected 14 | - triaged 15 | 16 | # Label to use when marking an issue as stale 17 | staleLabel: stale 18 | 19 | # Comment to post when marking an issue as stale. Set to `false` to disable 20 | markComment: > 21 | This issue has been automatically marked as stale because it has not had 22 | recent activity. It will be closed if no further activity occurs. Thank you 23 | for your contributions. 24 | 25 | # Comment to post when closing a stale issue. Set to `false` to disable 26 | closeComment: false -------------------------------------------------------------------------------- /internal/tests/backoff.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/cenkalti/backoff/v4" 9 | ) 10 | 11 | // RetryUntilAvailable calls callback repeatedly until callback return nil error 12 | // or until test timeout will be reached. 13 | func RetryUntilAvailable(t *testing.T, serviceName, addr string, f func(ctx context.Context) error) { 14 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 15 | defer cancel() 16 | 17 | bo := backoff.NewExponentialBackOff() 18 | bo.MaxInterval = time.Second * 5 19 | 20 | if err := backoff.Retry(func() error { 21 | t.Logf("Trying to connect to %s %s", serviceName, addr) 22 | return f(ctx) 23 | }, backoff.WithContext(bo, ctx)); err != nil { 24 | t.Fatalf("Could not connect to %s: %s", serviceName, err) 25 | return 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /storage/find.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | 8 | "github.com/gotd/td/telegram/query/dialogs" 9 | "github.com/gotd/td/tg" 10 | ) 11 | 12 | // FindPeer finds peer using given storage. 13 | func FindPeer(ctx context.Context, s PeerStorage, p tg.PeerClass) (Peer, error) { 14 | var key dialogs.DialogKey 15 | 16 | if err := key.FromPeer(p); err != nil { 17 | return Peer{}, err 18 | } 19 | 20 | return s.Find(ctx, PeerKey{ 21 | Kind: key.Kind, 22 | ID: key.ID, 23 | }) 24 | } 25 | 26 | // ForEach calls callback on every iterator element. 27 | func ForEach(ctx context.Context, iterator PeerIterator, cb func(Peer) error) error { 28 | for iterator.Next(ctx) { 29 | if err := cb(iterator.Value()); err != nil { 30 | return errors.Errorf("callback: %w", err) 31 | } 32 | } 33 | return iterator.Err() 34 | } 35 | -------------------------------------------------------------------------------- /s3/e2e_test.go: -------------------------------------------------------------------------------- 1 | package s3_test 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/minio/minio-go/v7" 9 | "github.com/minio/minio-go/v7/pkg/credentials" 10 | 11 | "github.com/gotd/contrib/internal/tests" 12 | "github.com/gotd/contrib/s3" 13 | ) 14 | 15 | func TestE2E(t *testing.T) { 16 | addr := os.Getenv("S3_ADDR") 17 | if addr == "" { 18 | t.Skip("Set S3_ADDR to run E2E test") 19 | } 20 | 21 | db, err := minio.New(addr, &minio.Options{ 22 | Creds: credentials.NewStaticV4( 23 | os.Getenv("MINIO_ACCESS_KEY"), 24 | os.Getenv("MINIO_SECRET_KEY"), 25 | "", 26 | ), 27 | }) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | tests.RetryUntilAvailable(t, "s3", addr, func(ctx context.Context) error { 32 | _, err := db.ListBuckets(ctx) 33 | return err 34 | }) 35 | 36 | tests.TestSessionStorage(t, s3.NewSessionStorage(db, "testsession", "session")) 37 | } 38 | -------------------------------------------------------------------------------- /auth/signup_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | tgauth "github.com/gotd/td/telegram/auth" 10 | "github.com/gotd/td/tg" 11 | ) 12 | 13 | func TestConstant(t *testing.T) { 14 | firstName := "first" 15 | lastName := "last" 16 | info := tgauth.UserInfo{ 17 | FirstName: firstName, 18 | LastName: lastName, 19 | } 20 | 21 | signUp := ConstantSignUp(info) 22 | ctx := context.Background() 23 | 24 | a := require.New(t) 25 | a.NoError(signUp.AcceptTermsOfService(ctx, tg.HelpTermsOfService{})) 26 | gotInfo, err := signUp.SignUp(ctx) 27 | a.NoError(err) 28 | a.Equal(info, gotInfo) 29 | } 30 | 31 | func TestNoSignUp(t *testing.T) { 32 | signUp := NoSignUp() 33 | ctx := context.Background() 34 | 35 | a := require.New(t) 36 | a.Error(signUp.AcceptTermsOfService(ctx, tg.HelpTermsOfService{})) 37 | _, err := signUp.SignUp(ctx) 38 | a.Error(err) 39 | } 40 | -------------------------------------------------------------------------------- /middleware/floodwait/scheduler_test.go: -------------------------------------------------------------------------------- 1 | package floodwait 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/gotd/neo" 10 | ) 11 | 12 | func TestScheduler(t *testing.T) { 13 | a := require.New(t) 14 | n := neo.NewTime(time.Now()) 15 | sch := newScheduler(n, time.Second) 16 | 17 | r := request{ 18 | key: 1, 19 | } 20 | // Schedule request. 21 | sch.schedule(r) 22 | // Got flood wait. 23 | sch.flood(r, 5*time.Second) 24 | // Ensure that request re-scheduled. 25 | a.Empty(sch.gather(nil)) 26 | 27 | // Travel and ensure that request re-scheduled correctly. 28 | n.Travel(5*time.Second + time.Millisecond) 29 | a.Len(sch.gather(nil), 1) 30 | 31 | // Decrease wait timeout. 32 | sch.nice(1) 33 | // Schedule yet one 34 | sch.schedule(request{ 35 | key: 1, 36 | }) 37 | // Ensure that timer decreased correctly. 38 | n.Travel(4*time.Second + time.Millisecond) 39 | a.Len(sch.gather(nil), 1) 40 | } 41 | -------------------------------------------------------------------------------- /invoker/update.go: -------------------------------------------------------------------------------- 1 | package invoker 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | 8 | "github.com/gotd/td/bin" 9 | "github.com/gotd/td/telegram" 10 | "github.com/gotd/td/tg" 11 | ) 12 | 13 | // UpdateHook middleware is called on each tg.UpdatesClass method result. 14 | // 15 | // Function is called before invoker return. Returned error will be wrapped 16 | // and returned as InvokeRaw result. 17 | type UpdateHook func(ctx context.Context, u tg.UpdatesClass) error 18 | 19 | // Handle implements telegram.Middleware. 20 | func (h UpdateHook) Handle(next tg.Invoker) telegram.InvokeFunc { 21 | return func(ctx context.Context, input bin.Encoder, output bin.Decoder) error { 22 | if err := next.Invoke(ctx, input, output); err != nil { 23 | return err 24 | } 25 | if u, ok := output.(*tg.UpdatesBox); ok { 26 | if err := h(ctx, u.Updates); err != nil { 27 | return errors.Errorf("hook: %w", err) 28 | } 29 | } 30 | 31 | return nil 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pebble/client.go: -------------------------------------------------------------------------------- 1 | package pebble 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cockroachdb/pebble" 7 | "github.com/go-faster/errors" 8 | "go.uber.org/multierr" 9 | 10 | "github.com/gotd/contrib/auth/kv" 11 | ) 12 | 13 | type pebbleStorage struct { 14 | db *pebble.DB 15 | opts *pebble.WriteOptions 16 | } 17 | 18 | func (p pebbleStorage) Set(ctx context.Context, k, v string) (rerr error) { 19 | b := p.db.NewBatch() 20 | defer func() { 21 | multierr.AppendInto(&rerr, b.Close()) 22 | }() 23 | 24 | d := b.SetDeferred(len(k), len(v)) 25 | copy(d.Key, k) 26 | copy(d.Value, v) 27 | _ = d.Finish() 28 | 29 | return b.Commit(p.opts) 30 | } 31 | 32 | func (p pebbleStorage) Get(ctx context.Context, k string) (string, error) { 33 | r, closer, err := p.db.Get([]byte(k)) 34 | if err != nil { 35 | if errors.Is(err, pebble.ErrNotFound) { 36 | return "", kv.ErrKeyNotFound 37 | } 38 | return "", err 39 | } 40 | v := string(r) 41 | 42 | return v, closer.Close() 43 | } 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Use this template for reporting bugs. 4 | title: "[SUBJECT]: [DESCRIPTION]" 5 | labels: bug 6 | --- 7 | 8 | 9 | 10 | ### What version of gotd are you using? 11 | 12 |
13 | $ go list -m github.com/gotd/td
14 | 
15 | 
16 | 17 | ### Does this issue reproduce with the latest release? 18 | 19 | 20 | 21 | ### What did you do? 22 | 23 | 27 | 28 | ### What did you expect to see? 29 | 30 | 31 | 32 | ### What did you see instead? 33 | 34 | 35 | 36 | ### What Go version and environment are you using? 37 | 38 |
39 | $ go version
40 | 
41 | 
42 | 43 |
go env Output
44 | $ go env
45 | 
46 | 
47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 tdakkota 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /auth/kv/session.go: -------------------------------------------------------------------------------- 1 | package kv 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | 8 | "github.com/gotd/td/session" 9 | ) 10 | 11 | var _ session.Storage = Session{} 12 | 13 | // Session is a generic implementation of session storage 14 | // over key-value Storage. 15 | type Session struct { 16 | storage Storage 17 | key string 18 | } 19 | 20 | // NewSession creates new Session. 21 | func NewSession(storage Storage, key string) Session { 22 | return Session{storage: storage, key: key} 23 | } 24 | 25 | // LoadSession loads session using given key from storage. 26 | func (s Session) LoadSession(ctx context.Context) ([]byte, error) { 27 | r, err := s.storage.Get(ctx, s.key) 28 | if err != nil { 29 | if errors.Is(err, ErrKeyNotFound) { 30 | return nil, session.ErrNotFound 31 | } 32 | return nil, err 33 | } 34 | 35 | return []byte(r), nil 36 | } 37 | 38 | // StoreSession saves session using given key to storage. 39 | func (s Session) StoreSession(ctx context.Context, data []byte) error { 40 | return s.storage.Set(ctx, s.key, string(data)) 41 | } 42 | -------------------------------------------------------------------------------- /clock/ntp.go: -------------------------------------------------------------------------------- 1 | // Package clock wraps clock sources. 2 | package clock 3 | 4 | import ( 5 | "fmt" 6 | "time" 7 | 8 | "github.com/gotd/td/clock" 9 | 10 | "github.com/beevik/ntp" 11 | ) 12 | 13 | var _ clock.Clock = (*ntpClock)(nil) 14 | 15 | const defaultNTP = "pool.ntp.org" 16 | 17 | type ntpClock struct { 18 | offset time.Duration 19 | } 20 | 21 | func (n *ntpClock) Now() time.Time { 22 | return time.Now().Add(n.offset) 23 | } 24 | 25 | func (n *ntpClock) Timer(d time.Duration) clock.Timer { 26 | return clock.System.Timer(d) 27 | } 28 | 29 | func (n *ntpClock) Ticker(d time.Duration) clock.Ticker { 30 | return clock.System.Ticker(d) 31 | } 32 | 33 | // NewNTP creates new NTP clock. 34 | func NewNTP(ntpHost ...string) (clock.Clock, error) { 35 | var host string 36 | switch len(ntpHost) { 37 | case 0: 38 | host = defaultNTP 39 | case 1: 40 | host = ntpHost[0] 41 | default: 42 | return nil, fmt.Errorf("too many ntp hosts") 43 | } 44 | 45 | resp, err := ntp.Query(host) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return &ntpClock{ 51 | offset: resp.ClockOffset, 52 | }, nil 53 | } 54 | -------------------------------------------------------------------------------- /bbolt/client.go: -------------------------------------------------------------------------------- 1 | package bbolt 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | "go.etcd.io/bbolt" 8 | 9 | "github.com/gotd/contrib/auth/kv" 10 | ) 11 | 12 | type bboltStorage struct { 13 | db *bbolt.DB 14 | bucket []byte 15 | } 16 | 17 | func (p bboltStorage) Set(ctx context.Context, k, v string) (rerr error) { 18 | return p.db.Batch(func(tx *bbolt.Tx) error { 19 | bucket, err := tx.CreateBucketIfNotExists(p.bucket) 20 | if err != nil { 21 | return errors.Errorf("create bucket: %w", err) 22 | } 23 | 24 | if err := bucket.Put([]byte(k), []byte(v)); err != nil { 25 | return errors.Errorf("put: %w", err) 26 | } 27 | return nil 28 | }) 29 | } 30 | 31 | func (p bboltStorage) Get(ctx context.Context, k string) (r string, err error) { 32 | err = p.db.View(func(tx *bbolt.Tx) error { 33 | bucket := tx.Bucket(p.bucket) 34 | if bucket == nil { 35 | return errors.Errorf("bucket %q does not exist", p.bucket) 36 | } 37 | 38 | result := bucket.Get([]byte(k)) 39 | if result == nil { 40 | return kv.ErrKeyNotFound 41 | } 42 | 43 | r = string(result) 44 | return nil 45 | }) 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /pebble/session_example_test.go: -------------------------------------------------------------------------------- 1 | package pebble_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | 9 | pebbledb "github.com/cockroachdb/pebble" 10 | "github.com/go-faster/errors" 11 | 12 | "github.com/gotd/td/telegram" 13 | 14 | "github.com/gotd/contrib/pebble" 15 | ) 16 | 17 | func pebbleStorage(ctx context.Context) error { 18 | db, err := pebbledb.Open("pebble.db", &pebbledb.Options{}) 19 | if err != nil { 20 | return errors.Errorf("create pebble storage: %w", err) 21 | } 22 | storage := pebble.NewSessionStorage(db, "session") 23 | 24 | client, err := telegram.ClientFromEnvironment(telegram.Options{ 25 | SessionStorage: storage, 26 | }) 27 | if err != nil { 28 | return errors.Errorf("create client: %w", err) 29 | } 30 | 31 | return client.Run(ctx, func(ctx context.Context) error { 32 | _, err := client.Auth().Bot(ctx, os.Getenv("BOT_TOKEN")) 33 | return err 34 | }) 35 | } 36 | 37 | func ExampleSessionStorage() { 38 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) 39 | defer cancel() 40 | 41 | if err := pebbleStorage(ctx); err != nil { 42 | _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) 43 | os.Exit(1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /vault/session_example_test.go: -------------------------------------------------------------------------------- 1 | package vault_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/go-faster/errors" 10 | "github.com/hashicorp/vault/api" 11 | 12 | "github.com/gotd/td/telegram" 13 | 14 | "github.com/gotd/contrib/vault" 15 | ) 16 | 17 | func vaultStorage(ctx context.Context) error { 18 | vaultClient, err := api.NewClient(api.DefaultConfig()) 19 | if err != nil { 20 | return errors.Errorf("create Vault client: %w", err) 21 | } 22 | storage := vault.NewSessionStorage(vaultClient, "cubbyhole/telegram/user", "session") 23 | 24 | client, err := telegram.ClientFromEnvironment(telegram.Options{ 25 | SessionStorage: storage, 26 | }) 27 | if err != nil { 28 | return errors.Errorf("create client: %w", err) 29 | } 30 | 31 | return client.Run(ctx, func(ctx context.Context) error { 32 | _, err := client.Auth().Bot(ctx, os.Getenv("BOT_TOKEN")) 33 | return err 34 | }) 35 | } 36 | 37 | func ExampleSessionStorage() { 38 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) 39 | defer cancel() 40 | 41 | if err := vaultStorage(ctx); err != nil { 42 | _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) 43 | os.Exit(1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /auth/build_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | tgauth "github.com/gotd/td/telegram/auth" 10 | 11 | "github.com/gotd/td/tg" 12 | ) 13 | 14 | func TestBuild(t *testing.T) { 15 | info := tgauth.UserInfo{ 16 | FirstName: "FirstName", 17 | LastName: "LastName", 18 | } 19 | signUp := ConstantSignUp(info) 20 | codeAsk := tgauth.CodeAuthenticatorFunc(func(context.Context, *tg.AuthSentCode) (string, error) { 21 | return "code", nil 22 | }) 23 | 24 | ask := BuildAsk( 25 | codeAsk, 26 | signUp, 27 | ) 28 | 29 | cred := tgauth.Constant("phone", "password", codeAsk) 30 | auth := Build(cred, ask) 31 | 32 | ctx := context.Background() 33 | a := require.New(t) 34 | 35 | code, err := auth.Code(ctx, nil) 36 | a.NoError(err) 37 | a.Equal("code", code) 38 | 39 | phone, err := auth.Phone(ctx) 40 | a.NoError(err) 41 | a.Equal("phone", phone) 42 | 43 | password, err := auth.Password(ctx) 44 | a.NoError(err) 45 | a.Equal("password", password) 46 | 47 | gotInfo, err := auth.SignUp(ctx) 48 | a.NoError(err) 49 | a.Equal(info, gotInfo) 50 | 51 | a.NoError(auth.AcceptTermsOfService(ctx, tg.HelpTermsOfService{})) 52 | } 53 | -------------------------------------------------------------------------------- /bbolt/session_example_test.go: -------------------------------------------------------------------------------- 1 | package bbolt_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/go-faster/errors" 10 | bboltdb "go.etcd.io/bbolt" 11 | 12 | "github.com/gotd/td/telegram" 13 | 14 | "github.com/gotd/contrib/bbolt" 15 | ) 16 | 17 | func bboltStorage(ctx context.Context) error { 18 | db, err := bboltdb.Open("bbolt.db", 0666, &bboltdb.Options{}) // nolint:gocritic 19 | if err != nil { 20 | return errors.Errorf("create bbolt storage: %w", err) 21 | } 22 | storage := bbolt.NewSessionStorage(db, "session", []byte("bucket")) 23 | 24 | client, err := telegram.ClientFromEnvironment(telegram.Options{ 25 | SessionStorage: storage, 26 | }) 27 | if err != nil { 28 | return errors.Errorf("create client: %w", err) 29 | } 30 | 31 | return client.Run(ctx, func(ctx context.Context) error { 32 | _, err := client.Auth().Bot(ctx, os.Getenv("BOT_TOKEN")) 33 | return err 34 | }) 35 | } 36 | 37 | func ExampleSessionStorage() { 38 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) 39 | defer cancel() 40 | 41 | if err := bboltStorage(ctx); err != nil { 42 | _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) 43 | os.Exit(1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /storage/peer_storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/go-faster/errors" 8 | ) 9 | 10 | // ErrPeerNotFound is a special error to return when peer not found. 11 | var ErrPeerNotFound = errors.New("peer not found") 12 | 13 | // PeerStorage is abstraction for peer storage. 14 | type PeerStorage interface { 15 | // Add adds given peer to the storage. 16 | Add(ctx context.Context, value Peer) error 17 | // Find finds peer using given key. 18 | // If peer not found, it returns ErrPeerNotFound error. 19 | Find(ctx context.Context, key PeerKey) (Peer, error) 20 | 21 | // Assign adds given peer to the storage and associates it to the given key. 22 | Assign(ctx context.Context, key string, value Peer) error 23 | // Resolve finds peer using associated key. 24 | // If peer not found, it returns ErrPeerNotFound error. 25 | Resolve(ctx context.Context, key string) (Peer, error) 26 | 27 | // Iterate creates and returns new PeerIterator. 28 | Iterate(ctx context.Context) (PeerIterator, error) 29 | } 30 | 31 | // PeerIterator is a peer iterator. 32 | type PeerIterator interface { 33 | Next(ctx context.Context) bool 34 | Err() error 35 | Value() Peer 36 | io.Closer 37 | } 38 | -------------------------------------------------------------------------------- /middleware/example_test.go: -------------------------------------------------------------------------------- 1 | package middleware_test 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "golang.org/x/time/rate" 8 | 9 | "github.com/gotd/td/telegram" 10 | "github.com/gotd/td/tg" 11 | 12 | "github.com/gotd/contrib/middleware/floodwait" 13 | "github.com/gotd/contrib/middleware/ratelimit" 14 | ) 15 | 16 | func Example() { 17 | // Create a new telegram.Client instance that handles FLOOD_WAIT errors 18 | // and limits request rate to 10 Hz with max burst size of 5 request. 19 | // 20 | // Note that you must not use test app credentials in production. 21 | // See https://core.telegram.org/api/obtaining_api_id 22 | client := telegram.NewClient( 23 | telegram.TestAppID, 24 | telegram.TestAppHash, 25 | telegram.Options{ 26 | Middlewares: []telegram.Middleware{ 27 | floodwait.NewSimpleWaiter().WithMaxRetries(10), 28 | ratelimit.New(rate.Every(100*time.Millisecond), 5), 29 | }, 30 | }, 31 | ) 32 | 33 | api := tg.NewClient(client) 34 | ctx := context.TODO() 35 | err := client.Run(ctx, func(ctx context.Context) error { 36 | _, err := api.ContactsResolveUsername(ctx, &tg.ContactsResolveUsernameRequest{ 37 | Username: "@self", 38 | }) 39 | return err 40 | }) 41 | if err != nil { 42 | panic(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /vault/e2e_test.go: -------------------------------------------------------------------------------- 1 | package vault_test 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "encoding/hex" 7 | "io" 8 | "os" 9 | "testing" 10 | 11 | "github.com/hashicorp/vault/api" 12 | 13 | "github.com/gotd/contrib/internal/tests" 14 | "github.com/gotd/contrib/vault" 15 | ) 16 | 17 | func TestE2E(t *testing.T) { 18 | addr := os.Getenv("VAULT_ADDR") 19 | if addr == "" { 20 | t.Skip("Set VAULT_ADDR to run E2E test") 21 | } 22 | 23 | token, ok := os.LookupEnv("VAULT_TOKEN") 24 | if !ok { 25 | var data [16]byte 26 | if _, err := io.ReadFull(rand.Reader, data[:]); err != nil { 27 | t.Fatalf("Failed to generate token: %s", err) 28 | } 29 | token = hex.EncodeToString(data[:]) 30 | } 31 | 32 | cfg := api.DefaultConfig() 33 | cfg.Address = addr 34 | client, err := api.NewClient(cfg) 35 | if err != nil { 36 | t.Fatalf("Can't create client: %s", err) 37 | return 38 | } 39 | client.SetToken(token) 40 | 41 | tests.RetryUntilAvailable(t, "Vault", addr, func(ctx context.Context) error { 42 | _, err := client.Sys().Health() 43 | return err 44 | }) 45 | 46 | tests.TestSessionStorage(t, vault.NewSessionStorage(client, "cubbyhole/testsession", "session")) 47 | tests.TestCredentials(t, vault.NewCredentials(client, "cubbyhole/testauth")) 48 | } 49 | -------------------------------------------------------------------------------- /storage/hook.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | 6 | "go.uber.org/multierr" 7 | 8 | "github.com/gotd/td/telegram" 9 | "github.com/gotd/td/tg" 10 | ) 11 | 12 | type updateHook struct { 13 | next telegram.UpdateHandler 14 | storage PeerStorage 15 | } 16 | 17 | type updatesWithPeers interface { 18 | GetUsers() []tg.UserClass 19 | GetChats() []tg.ChatClass 20 | tg.UpdatesClass 21 | } 22 | 23 | func (h updateHook) Handle(ctx context.Context, u tg.UpdatesClass) error { 24 | updates, ok := u.(updatesWithPeers) 25 | if !ok { 26 | return h.next.Handle(ctx, u) 27 | } 28 | 29 | var rerr error 30 | for _, chat := range updates.GetChats() { 31 | if value := (Peer{}); value.FromChat(chat) { 32 | multierr.AppendInto(&rerr, h.storage.Add(ctx, value)) 33 | } 34 | } 35 | 36 | for _, user := range updates.GetUsers() { 37 | if value := (Peer{}); value.FromUser(user) { 38 | multierr.AppendInto(&rerr, h.storage.Add(ctx, value)) 39 | } 40 | } 41 | 42 | return multierr.Append(rerr, h.next.Handle(ctx, u)) 43 | } 44 | 45 | // UpdateHook creates update hook, to collect peer data from updates. 46 | func UpdateHook(next telegram.UpdateHandler, storage PeerStorage) telegram.UpdateHandler { 47 | return updateHook{ 48 | next: next, 49 | storage: storage, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /redis/auth_example_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/go-faster/errors" 10 | redisclient "github.com/go-redis/redis/v8" 11 | 12 | tgauth "github.com/gotd/td/telegram/auth" 13 | 14 | "github.com/gotd/td/telegram" 15 | 16 | "github.com/gotd/contrib/auth" 17 | "github.com/gotd/contrib/auth/terminal" 18 | "github.com/gotd/contrib/redis" 19 | ) 20 | 21 | func redisAuth(ctx context.Context) error { 22 | redisClient := redisclient.NewClient(&redisclient.Options{}) 23 | cred := redis.NewCredentials(redisClient). 24 | WithPhoneKey("phone"). 25 | WithPasswordKey("password") 26 | 27 | client, err := telegram.ClientFromEnvironment(telegram.Options{}) 28 | if err != nil { 29 | return errors.Errorf("create client: %w", err) 30 | } 31 | 32 | return client.Run(ctx, func(ctx context.Context) error { 33 | return client.Auth().IfNecessary( 34 | ctx, 35 | tgauth.NewFlow(auth.Build(cred, terminal.OS()), tgauth.SendCodeOptions{}), 36 | ) 37 | }) 38 | } 39 | 40 | func ExampleCredentials() { 41 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) 42 | defer cancel() 43 | 44 | if err := redisAuth(ctx); err != nil { 45 | _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) 46 | os.Exit(1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /storage/peer_as.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/gotd/td/telegram/query/dialogs" 5 | "github.com/gotd/td/tg" 6 | ) 7 | 8 | // AsInputUser tries to convert peer to *tg.InputUser. 9 | func (p Peer) AsInputUser() (*tg.InputUser, bool) { 10 | if p.Key.Kind != dialogs.User { 11 | return nil, false 12 | } 13 | 14 | return &tg.InputUser{ 15 | UserID: p.Key.ID, 16 | AccessHash: p.Key.AccessHash, 17 | }, true 18 | } 19 | 20 | // AsInputChannel tries to convert peer to *tg.InputChannel. 21 | func (p Peer) AsInputChannel() (*tg.InputChannel, bool) { 22 | if p.Key.Kind != dialogs.Channel { 23 | return nil, false 24 | } 25 | 26 | return &tg.InputChannel{ 27 | ChannelID: p.Key.ID, 28 | AccessHash: p.Key.AccessHash, 29 | }, true 30 | } 31 | 32 | // AsInputPeer tries to convert peer to tg.InputPeerClass. 33 | func (p Peer) AsInputPeer() tg.InputPeerClass { 34 | switch p.Key.Kind { 35 | case dialogs.User: 36 | return &tg.InputPeerUser{ 37 | UserID: p.Key.ID, 38 | AccessHash: p.Key.AccessHash, 39 | } 40 | case dialogs.Chat: 41 | return &tg.InputPeerChat{ 42 | ChatID: p.Key.ID, 43 | } 44 | case dialogs.Channel: 45 | return &tg.InputPeerChannel{ 46 | ChannelID: p.Key.ID, 47 | AccessHash: p.Key.AccessHash, 48 | } 49 | default: 50 | panic("unreachable") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /auth/terminal/code_test.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | "golang.org/x/text/language" 11 | "golang.org/x/text/message" 12 | 13 | "github.com/gotd/td/tg" 14 | 15 | "github.com/gotd/contrib/auth/localization" 16 | ) 17 | 18 | func TestTerminal(t *testing.T) { 19 | ctx := context.Background() 20 | a := require.New(t) 21 | 22 | var in, out bytes.Buffer 23 | term := New(&in, &out).WithPrinter(message.NewPrinter(language.English)) 24 | test := func(output, input string, call func(t *Terminal) (string, error)) { 25 | in.WriteString(input + "\r") 26 | phone, err := call(term) 27 | a.NoError(err) 28 | a.Equal(input, phone) 29 | a.Equal(output+":"+input, strings.TrimSpace(out.String())) 30 | out.Reset() 31 | } 32 | 33 | input := "abc" 34 | test(localization.PhoneDialogPrompt, input, func(t *Terminal) (string, error) { 35 | return t.Phone(ctx) 36 | }) 37 | test(localization.PasswordDialogPrompt, input, func(t *Terminal) (string, error) { 38 | return t.Password(ctx) 39 | }) 40 | test(localization.CodeDialogPrompt, input, func(t *Terminal) (string, error) { 41 | return t.Code(ctx, &tg.AuthSentCode{ 42 | Type: &tg.AuthSentCodeTypeApp{ 43 | Length: len(input), 44 | }, 45 | }) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /oteltg/middleware_test.go: -------------------------------------------------------------------------------- 1 | package oteltg 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | metricnoop "go.opentelemetry.io/otel/metric/noop" 9 | tracenoop "go.opentelemetry.io/otel/trace/noop" 10 | 11 | "github.com/gotd/td/bin" 12 | "github.com/gotd/td/tg" 13 | "github.com/gotd/td/tgerr" 14 | ) 15 | 16 | type invoker func(ctx context.Context, input bin.Encoder, output bin.Decoder) error 17 | 18 | func (i invoker) Invoke(ctx context.Context, input bin.Encoder, output bin.Decoder) error { 19 | return i(ctx, input, output) 20 | } 21 | 22 | func TestMiddleware_Handle(t *testing.T) { 23 | m, err := New(metricnoop.NewMeterProvider(), tracenoop.NewTracerProvider()) 24 | require.NoError(t, err) 25 | 26 | okInvoker := invoker(func(ctx context.Context, input bin.Encoder, output bin.Decoder) error { 27 | return nil 28 | }) 29 | errInvoker := invoker(func(ctx context.Context, input bin.Encoder, output bin.Decoder) error { 30 | return tgerr.New(0, tgerr.ErrFloodWait) 31 | }) 32 | 33 | ctx := context.Background() 34 | input := &tg.UsersGetUsersRequest{} 35 | require.NoError(t, m.Handle(okInvoker).Invoke(ctx, input, nil)) 36 | require.NoError(t, m.Handle(okInvoker).Invoke(ctx, nil, nil)) 37 | require.True(t, tgerr.Is(m.Handle(errInvoker).Invoke(ctx, input, nil), tgerr.ErrFloodWait)) 38 | } 39 | -------------------------------------------------------------------------------- /redis/session_example_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/go-faster/errors" 10 | redisclient "github.com/go-redis/redis/v8" 11 | 12 | "github.com/gotd/td/telegram" 13 | 14 | "github.com/gotd/contrib/redis" 15 | ) 16 | 17 | func redisStorage(ctx context.Context) error { 18 | redisClient := redisclient.NewClient(&redisclient.Options{}) 19 | storage := redis.NewSessionStorage(redisClient, "session") 20 | 21 | client, err := telegram.ClientFromEnvironment(telegram.Options{ 22 | SessionStorage: storage, 23 | }) 24 | if err != nil { 25 | return errors.Errorf("create client: %w", err) 26 | } 27 | 28 | return client.Run(ctx, func(ctx context.Context) error { 29 | // Force redis to flush DB. 30 | // It may be necessary to be sure that session will be saved to the disk. 31 | if err := redisClient.FlushDBAsync(ctx).Err(); err != nil { 32 | return errors.Errorf("flush: %w", err) 33 | } 34 | 35 | _, err := client.Auth().Bot(ctx, os.Getenv("BOT_TOKEN")) 36 | return err 37 | }) 38 | } 39 | 40 | func ExampleSessionStorage() { 41 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) 42 | defer cancel() 43 | 44 | if err := redisStorage(ctx); err != nil { 45 | _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) 46 | os.Exit(1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /storage/collector_example_test.go: -------------------------------------------------------------------------------- 1 | package storage_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | 9 | pebbledb "github.com/cockroachdb/pebble" 10 | "github.com/go-faster/errors" 11 | 12 | "github.com/gotd/td/telegram" 13 | "github.com/gotd/td/telegram/query" 14 | "github.com/gotd/td/tg" 15 | 16 | "github.com/gotd/contrib/pebble" 17 | "github.com/gotd/contrib/storage" 18 | ) 19 | 20 | func peerCollector(ctx context.Context) error { 21 | db, err := pebbledb.Open("pebble.db", &pebbledb.Options{}) 22 | if err != nil { 23 | return errors.Errorf("create pebble storage: %w", err) 24 | } 25 | s := pebble.NewPeerStorage(db) 26 | collector := storage.CollectPeers(s) 27 | 28 | client, err := telegram.ClientFromEnvironment(telegram.Options{}) 29 | if err != nil { 30 | return errors.Errorf("create client: %w", err) 31 | } 32 | raw := tg.NewClient(client) 33 | 34 | return client.Run(ctx, func(ctx context.Context) error { 35 | // Fills storage with user dialogs peers metadata. 36 | return collector.Dialogs(ctx, query.GetDialogs(raw).Iter()) 37 | }) 38 | } 39 | 40 | func ExampleCollectPeers() { 41 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) 42 | defer cancel() 43 | 44 | if err := peerCollector(ctx); err != nil { 45 | _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) 46 | os.Exit(1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /storage/key_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/gotd/td/telegram/query/dialogs" 10 | 11 | "github.com/gotd/td/tg" 12 | ) 13 | 14 | func TestKey(t *testing.T) { 15 | a := require.New(t) 16 | var p Peer 17 | a.NoError(p.FromInputPeer(&tg.InputPeerUser{ 18 | UserID: 10, 19 | AccessHash: 10, 20 | })) 21 | k := KeyFromPeer(p) 22 | 23 | b := k.Bytes(nil) 24 | a.NoError(k.Parse(b)) 25 | 26 | s := k.String() 27 | sBytes := []byte(s) 28 | a.Equal(b, sBytes) 29 | a.NoError(k.Parse(sBytes)) 30 | } 31 | 32 | func TestKey_Parse(t *testing.T) { 33 | tests := []struct { 34 | fields PeerKey 35 | arg string 36 | wantErr bool 37 | }{ 38 | {PeerKey{}, "_10", true}, 39 | {PeerKey{}, "10", true}, 40 | {PeerKey{}, "10_", true}, 41 | {PeerKey{}, "10_1", true}, 42 | {PeerKey{}, "peer10_1", true}, 43 | {PeerKey{ 44 | Kind: dialogs.Channel, 45 | ID: 1, 46 | }, "peer" + strconv.Itoa(int(dialogs.Channel)) + "_1", false}, 47 | } 48 | for _, tt := range tests { 49 | t.Run(tt.arg, func(t *testing.T) { 50 | k := PeerKey{} 51 | 52 | err := k.Parse([]byte(tt.arg)) 53 | if tt.wantErr { 54 | require.Error(t, err) 55 | } else { 56 | require.NoError(t, err) 57 | require.Equal(t, tt.fields, k) 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pebble/auth_example_test.go: -------------------------------------------------------------------------------- 1 | package pebble_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | 9 | pebbledb "github.com/cockroachdb/pebble" 10 | "github.com/go-faster/errors" 11 | 12 | tgauth "github.com/gotd/td/telegram/auth" 13 | 14 | "github.com/gotd/td/telegram" 15 | 16 | "github.com/gotd/contrib/auth" 17 | "github.com/gotd/contrib/auth/terminal" 18 | "github.com/gotd/contrib/pebble" 19 | ) 20 | 21 | func pebbleAuth(ctx context.Context) error { 22 | db, err := pebbledb.Open("pebble.db", &pebbledb.Options{}) 23 | if err != nil { 24 | return errors.Errorf("create pebble storage: %w", err) 25 | } 26 | cred := pebble.NewCredentials(db). 27 | WithPhoneKey("phone"). 28 | WithPasswordKey("password") 29 | 30 | client, err := telegram.ClientFromEnvironment(telegram.Options{}) 31 | if err != nil { 32 | return errors.Errorf("create client: %w", err) 33 | } 34 | 35 | return client.Run(ctx, func(ctx context.Context) error { 36 | return client.Auth().IfNecessary( 37 | ctx, 38 | tgauth.NewFlow(auth.Build(cred, terminal.OS()), tgauth.SendCodeOptions{}), 39 | ) 40 | }) 41 | } 42 | 43 | func ExampleCredentials() { 44 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) 45 | defer cancel() 46 | 47 | if err := pebbleAuth(ctx); err != nil { 48 | _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) 49 | os.Exit(1) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /vault/auth_example_test.go: -------------------------------------------------------------------------------- 1 | package vault_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/go-faster/errors" 10 | "github.com/hashicorp/vault/api" 11 | 12 | "github.com/gotd/td/telegram" 13 | tgauth "github.com/gotd/td/telegram/auth" 14 | 15 | "github.com/gotd/contrib/auth" 16 | "github.com/gotd/contrib/auth/terminal" 17 | "github.com/gotd/contrib/vault" 18 | ) 19 | 20 | func vaultAuth(ctx context.Context) error { 21 | vaultClient, err := api.NewClient(api.DefaultConfig()) 22 | if err != nil { 23 | return errors.Errorf("create Vault client: %w", err) 24 | } 25 | cred := vault.NewCredentials(vaultClient, "cubbyhole/telegram/user"). 26 | WithPhoneKey("phone"). 27 | WithPasswordKey("password") 28 | 29 | client, err := telegram.ClientFromEnvironment(telegram.Options{}) 30 | if err != nil { 31 | return errors.Errorf("create client: %w", err) 32 | } 33 | 34 | return client.Run(ctx, func(ctx context.Context) error { 35 | return client.Auth().IfNecessary( 36 | ctx, 37 | tgauth.NewFlow(auth.Build(cred, terminal.OS()), tgauth.SendCodeOptions{}), 38 | ) 39 | }) 40 | } 41 | 42 | func ExampleCredentials() { 43 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) 44 | defer cancel() 45 | 46 | if err := vaultAuth(ctx); err != nil { 47 | _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) 48 | os.Exit(1) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /bbolt/auth_example_test.go: -------------------------------------------------------------------------------- 1 | package bbolt_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/go-faster/errors" 10 | bboltdb "go.etcd.io/bbolt" 11 | 12 | "github.com/gotd/td/telegram" 13 | tgauth "github.com/gotd/td/telegram/auth" 14 | 15 | "github.com/gotd/contrib/auth" 16 | "github.com/gotd/contrib/auth/terminal" 17 | "github.com/gotd/contrib/bbolt" 18 | ) 19 | 20 | func bboltAuth(ctx context.Context) error { 21 | db, err := bboltdb.Open("bbolt.db", 0666, &bboltdb.Options{}) // nolint:gocritic 22 | if err != nil { 23 | return errors.Errorf("create bbolt storage: %w", err) 24 | } 25 | cred := bbolt.NewCredentials(db, []byte("bucket")). 26 | WithPhoneKey("phone"). 27 | WithPasswordKey("password") 28 | 29 | client, err := telegram.ClientFromEnvironment(telegram.Options{}) 30 | if err != nil { 31 | return errors.Errorf("create client: %w", err) 32 | } 33 | 34 | return client.Run(ctx, func(ctx context.Context) error { 35 | return client.Auth().IfNecessary( 36 | ctx, 37 | tgauth.NewFlow(auth.Build(cred, terminal.OS()), tgauth.SendCodeOptions{}), 38 | ) 39 | }) 40 | } 41 | 42 | func ExampleCredentials() { 43 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) 44 | defer cancel() 45 | 46 | if err := bboltAuth(ctx); err != nil { 47 | _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) 48 | os.Exit(1) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /s3/session_example_test.go: -------------------------------------------------------------------------------- 1 | package s3_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/go-faster/errors" 10 | "github.com/minio/minio-go/v7" 11 | "github.com/minio/minio-go/v7/pkg/credentials" 12 | 13 | "github.com/gotd/td/telegram" 14 | 15 | "github.com/gotd/contrib/s3" 16 | ) 17 | 18 | func s3Storage(ctx context.Context) error { 19 | accessKeyID := "Q3AM3UQ867SPQQA43P2F" 20 | secretAccessKey := "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" 21 | 22 | db, err := minio.New("play.min.io", &minio.Options{ 23 | Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""), 24 | }) 25 | if err != nil { 26 | return errors.Errorf("create s3 storage: %w", err) 27 | } 28 | storage := s3.NewSessionStorage(db, "telegram", "session") 29 | 30 | client, err := telegram.ClientFromEnvironment(telegram.Options{ 31 | SessionStorage: storage, 32 | }) 33 | if err != nil { 34 | return errors.Errorf("create client: %w", err) 35 | } 36 | 37 | return client.Run(ctx, func(ctx context.Context) error { 38 | _, err := client.Auth().Bot(ctx, os.Getenv("BOT_TOKEN")) 39 | return err 40 | }) 41 | } 42 | 43 | func ExampleSessionStorage() { 44 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) 45 | defer cancel() 46 | 47 | if err := s3Storage(ctx); err != nil { 48 | _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) 49 | os.Exit(1) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /storage/resolver_cache_example_test.go: -------------------------------------------------------------------------------- 1 | package storage_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | 9 | pebbledb "github.com/cockroachdb/pebble" 10 | "github.com/go-faster/errors" 11 | 12 | "github.com/gotd/td/telegram/message" 13 | "github.com/gotd/td/telegram/message/peer" 14 | "github.com/gotd/td/tg" 15 | 16 | "github.com/gotd/td/telegram" 17 | 18 | "github.com/gotd/contrib/pebble" 19 | "github.com/gotd/contrib/storage" 20 | ) 21 | 22 | func resolverCache(ctx context.Context) error { 23 | db, err := pebbledb.Open("pebble.db", &pebbledb.Options{}) 24 | if err != nil { 25 | return errors.Errorf("create pebble storage: %w", err) 26 | } 27 | 28 | client, err := telegram.ClientFromEnvironment(telegram.Options{}) 29 | if err != nil { 30 | return errors.Errorf("create client: %w", err) 31 | } 32 | 33 | return client.Run(ctx, func(ctx context.Context) error { 34 | raw := tg.NewClient(client) 35 | resolver := storage.NewResolverCache(peer.Plain(raw), pebble.NewPeerStorage(db)) 36 | s := message.NewSender(raw).WithResolver(resolver) 37 | 38 | _, err := s.Resolve("durov").Text(ctx, "Hi!") 39 | return err 40 | }) 41 | } 42 | 43 | func ExampleResolverCache() { 44 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) 45 | defer cancel() 46 | 47 | if err := resolverCache(ctx); err != nil { 48 | _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) 49 | os.Exit(1) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /http_range/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /tg_io/download.go: -------------------------------------------------------------------------------- 1 | // Package tg_io implements partial i/o using telegram. 2 | package tg_io 3 | 4 | import ( 5 | "context" 6 | "io" 7 | 8 | "github.com/go-faster/errors" 9 | 10 | "github.com/gotd/td/tg" 11 | 12 | "github.com/gotd/contrib/partio" 13 | ) 14 | 15 | // Downloader implements streamable file downloads of Telegram files. 16 | type Downloader struct { 17 | api *tg.Client 18 | } 19 | 20 | // NewDownloader creates new Downloader. 21 | func NewDownloader(api *tg.Client) *Downloader { 22 | return &Downloader{ 23 | api: api, 24 | } 25 | } 26 | 27 | // ChunkSource creates new chunk source for provided file. 28 | func (d *Downloader) ChunkSource(size int64, loc tg.InputFileLocationClass) partio.ChunkSource { 29 | return &chunkSource{ 30 | loc: loc, 31 | api: d.api, 32 | size: size, 33 | } 34 | } 35 | 36 | type chunkSource struct { 37 | loc tg.InputFileLocationClass 38 | api *tg.Client 39 | size int64 40 | } 41 | 42 | // Chunk implements partio.ChunkSource. 43 | func (s chunkSource) Chunk(ctx context.Context, offset int64, b []byte) (int64, error) { 44 | req := &tg.UploadGetFileRequest{ 45 | Offset: offset, 46 | Limit: len(b), 47 | Location: s.loc, 48 | } 49 | req.SetPrecise(true) 50 | 51 | r, err := s.api.UploadGetFile(ctx, req) 52 | if err != nil { 53 | return 0, err 54 | } 55 | 56 | switch result := r.(type) { 57 | case *tg.UploadFile: 58 | n := int64(copy(b, result.Bytes)) 59 | 60 | var err error 61 | if req.Offset+n >= s.size { 62 | // No more data. 63 | err = io.EOF 64 | } 65 | 66 | return n, err 67 | default: 68 | return 0, errors.Errorf("unexpected type %T", r) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /storage/find_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/gotd/td/tg" 10 | ) 11 | 12 | func TestFindPeer(t *testing.T) { 13 | a := require.New(t) 14 | ctx := context.Background() 15 | s := newMemStorage() 16 | 17 | var p Peer 18 | a.NoError(p.FromInputPeer(&tg.InputPeerUser{ 19 | UserID: 10, 20 | AccessHash: 10, 21 | })) 22 | a.NoError(s.Assign(ctx, "domain", p)) 23 | 24 | p2, err := FindPeer(ctx, s, &tg.PeerUser{ 25 | UserID: 10, 26 | }) 27 | a.NoError(err) 28 | a.Equal(p, p2) 29 | } 30 | 31 | type testIterator struct { 32 | buf []Peer 33 | cursor int 34 | } 35 | 36 | func (t *testIterator) Next(ctx context.Context) bool { 37 | if t.cursor < len(t.buf) { 38 | t.cursor++ 39 | return true 40 | } 41 | 42 | return false 43 | } 44 | 45 | func (t *testIterator) Err() error { 46 | return nil 47 | } 48 | 49 | func (t *testIterator) Value() Peer { 50 | return t.buf[t.cursor-1] 51 | } 52 | 53 | func (t *testIterator) Close() error { 54 | return nil 55 | } 56 | 57 | func TestForEach(t *testing.T) { 58 | a := require.New(t) 59 | 60 | buf := func() (r []Peer) { 61 | for i := range [5]struct{}{} { 62 | var p Peer 63 | a.NoError(p.FromInputPeer(&tg.InputPeerUser{ 64 | UserID: int64(i) + 11, 65 | AccessHash: int64(i) + 11, 66 | })) 67 | r = append(r, p) 68 | } 69 | 70 | return 71 | }() 72 | 73 | iter := &testIterator{ 74 | buf: buf, 75 | cursor: 0, 76 | } 77 | 78 | i := 0 79 | a.NoError(ForEach(context.Background(), iter, func(peer Peer) error { 80 | a.Equal(buf[i], peer) 81 | i++ 82 | return nil 83 | })) 84 | } 85 | -------------------------------------------------------------------------------- /invoker/debug.go: -------------------------------------------------------------------------------- 1 | package invoker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | "time" 9 | 10 | "go.uber.org/multierr" 11 | 12 | "github.com/gotd/td/bin" 13 | "github.com/gotd/td/tdp" 14 | "github.com/gotd/td/tg" 15 | ) 16 | 17 | // Debug is pretty-print debugging invoker middleware. 18 | type Debug struct { 19 | next tg.Invoker 20 | out io.Writer 21 | } 22 | 23 | // NewDebug creates new Debug middleware. 24 | func NewDebug(next tg.Invoker) *Debug { 25 | return &Debug{next: next} 26 | } 27 | 28 | // WithOutput sets output writer. 29 | func (d *Debug) WithOutput(out io.Writer) *Debug { 30 | d.out = out 31 | return d 32 | } 33 | 34 | func formatObject(input interface{}) string { 35 | o, ok := input.(tdp.Object) 36 | if !ok { 37 | // Handle tg.*Box values. 38 | rv := reflect.Indirect(reflect.ValueOf(input)) 39 | for i := 0; i < rv.NumField(); i++ { 40 | if v, ok := rv.Field(i).Interface().(tdp.Object); ok { 41 | return formatObject(v) 42 | } 43 | } 44 | 45 | return fmt.Sprintf("%T (not object)", input) 46 | } 47 | return tdp.Format(o) 48 | } 49 | 50 | // Invoke implements tg.Invoker. 51 | func (d *Debug) Invoke(ctx context.Context, input bin.Encoder, output bin.Decoder) error { 52 | _, rerr := fmt.Fprintln(d.out, "→", formatObject(input)) 53 | 54 | start := time.Now() 55 | if err := d.next.Invoke(ctx, input, output); err != nil { 56 | rerr = multierr.Append(rerr, err) 57 | _, err := fmt.Fprintln(d.out, "←", err) 58 | return multierr.Append(rerr, err) 59 | } 60 | 61 | _, err := fmt.Fprintf(d.out, 62 | "← (%s) %s\n", 63 | time.Since(start).Round(time.Millisecond), 64 | formatObject(output), 65 | ) 66 | return multierr.Append(rerr, err) 67 | } 68 | -------------------------------------------------------------------------------- /s3/session.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | 8 | "github.com/go-faster/errors" 9 | "github.com/minio/minio-go/v7" 10 | 11 | "github.com/gotd/td/session" 12 | ) 13 | 14 | var _ session.Storage = SessionStorage{} 15 | 16 | // SessionStorage is a MTProto session S3 storage. 17 | type SessionStorage struct { 18 | client *minio.Client 19 | bucketName, objectName string 20 | } 21 | 22 | // NewSessionStorage creates new SessionStorage. 23 | func NewSessionStorage(client *minio.Client, bucketName, objectName string) SessionStorage { 24 | return SessionStorage{ 25 | client: client, 26 | bucketName: bucketName, 27 | objectName: objectName, 28 | } 29 | } 30 | 31 | // LoadSession implements session.Storage. 32 | func (s SessionStorage) LoadSession(ctx context.Context) ([]byte, error) { 33 | obj, err := s.client.GetObject(ctx, s.bucketName, s.objectName, minio.GetObjectOptions{}) 34 | if err != nil { 35 | return nil, errors.Errorf("get %q/%q: %w", s.bucketName, s.objectName, err) 36 | } 37 | return io.ReadAll(obj) 38 | } 39 | 40 | // StoreSession implements session.Storage. 41 | func (s SessionStorage) StoreSession(ctx context.Context, data []byte) error { 42 | if err := s.client.MakeBucket(ctx, s.bucketName, minio.MakeBucketOptions{}); err != nil { 43 | return errors.Errorf("create bucket %q: %w", s.bucketName, err) 44 | } 45 | 46 | _, err := s.client.PutObject(ctx, s.bucketName, s.objectName, 47 | bytes.NewReader(data), int64(len(data)), 48 | minio.PutObjectOptions{ 49 | ContentType: "application/json", 50 | NumThreads: 1, 51 | }, 52 | ) 53 | if err != nil { 54 | return errors.Errorf("put %q/%q: %w", s.bucketName, s.objectName, err) 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /storage/hook_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/go-faster/errors" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/gotd/td/tg" 11 | ) 12 | 13 | type testHandler struct { 14 | returnErr error 15 | } 16 | 17 | func (t testHandler) Handle(ctx context.Context, u tg.UpdatesClass) error { 18 | return t.returnErr 19 | } 20 | 21 | func TestUpdateHook(t *testing.T) { 22 | ctx := context.Background() 23 | testData := &tg.Updates{ 24 | Chats: []tg.ChatClass{ 25 | &tg.Channel{ 26 | ID: 10, 27 | AccessHash: 10, 28 | Username: "channel", 29 | }, 30 | &tg.ChannelForbidden{ 31 | ID: 11, 32 | AccessHash: 11, 33 | }, 34 | }, 35 | Users: []tg.UserClass{ 36 | &tg.User{ 37 | ID: 10, 38 | AccessHash: 10, 39 | Username: "username", 40 | }, 41 | }, 42 | } 43 | 44 | t.Run("Good", func(t *testing.T) { 45 | a := require.New(t) 46 | storage := newMemStorage() 47 | h := UpdateHook(testHandler{}, storage) 48 | 49 | a.NoError(h.Handle(ctx, testData)) 50 | 51 | p, err := storage.Resolve(ctx, "channel") 52 | a.NoError(err) 53 | a.NotNil(p.Channel) 54 | 55 | p, err = storage.Resolve(ctx, "username") 56 | a.NoError(err) 57 | a.NotNil(p.User) 58 | }) 59 | 60 | t.Run("Error", func(t *testing.T) { 61 | a := require.New(t) 62 | storage := newMemStorage() 63 | h := UpdateHook(testHandler{ 64 | returnErr: errors.New("testErr"), 65 | }, storage) 66 | 67 | a.Error(h.Handle(ctx, testData)) 68 | 69 | p, err := storage.Resolve(ctx, "channel") 70 | a.NoError(err) 71 | a.NotNil(p.Channel) 72 | 73 | p, err = storage.Resolve(ctx, "username") 74 | a.NoError(err) 75 | a.NotNil(p.User) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /auth/signup.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | 8 | tgauth "github.com/gotd/td/telegram/auth" 9 | "github.com/gotd/td/tg" 10 | ) 11 | 12 | // SignUpFlow is abstraction for user signup setup. 13 | type SignUpFlow interface { 14 | AcceptTermsOfService(ctx context.Context, tos tg.HelpTermsOfService) error 15 | SignUp(ctx context.Context) (tgauth.UserInfo, error) 16 | } 17 | 18 | // AutoAccept is noop implementation of AcceptTermsOfService call. 19 | type AutoAccept struct{} 20 | 21 | // AcceptTermsOfService partly implements SignUpFlow. 22 | func (AutoAccept) AcceptTermsOfService(ctx context.Context, tos tg.HelpTermsOfService) error { 23 | return nil 24 | } 25 | 26 | type constantSignUp struct { 27 | info tgauth.UserInfo 28 | AutoAccept 29 | } 30 | 31 | func (c constantSignUp) SignUp(ctx context.Context) (tgauth.UserInfo, error) { 32 | return c.info, nil 33 | } 34 | 35 | // ConstantSignUp creates new SignUpFlow using given User info. 36 | func ConstantSignUp(info tgauth.UserInfo) SignUpFlow { 37 | return constantSignUp{info: info} 38 | } 39 | 40 | // ErrSignUpIsNotExpected is returned, when sign up request from Telegram server 41 | // is not expected. 42 | var ErrSignUpIsNotExpected = errors.New("signup call is not expected") 43 | 44 | type noSignUp struct{} 45 | 46 | func (n noSignUp) AcceptTermsOfService(ctx context.Context, tos tg.HelpTermsOfService) error { 47 | return &tgauth.SignUpRequired{TermsOfService: tos} 48 | } 49 | 50 | func (n noSignUp) SignUp(ctx context.Context) (tgauth.UserInfo, error) { 51 | return tgauth.UserInfo{}, ErrSignUpIsNotExpected 52 | } 53 | 54 | // NoSignUp creates new SignUpFlow which returns ErrSignUpIsNotExpected. 55 | func NoSignUp() SignUpFlow { 56 | return noSignUp{} 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | schedule: 10 | - cron: '0 17 * * 5' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: [ 'go' ] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v5.0.0 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v3 42 | with: 43 | languages: ${{ matrix.language }} 44 | config-file: ./.github/codeql/config.yml 45 | 46 | - name: Perform CodeQL Analysis 47 | uses: github/codeql-action/analyze@v3 48 | -------------------------------------------------------------------------------- /auth/kv/auth.go: -------------------------------------------------------------------------------- 1 | package kv 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | 8 | tgauth "github.com/gotd/td/telegram/auth" 9 | ) 10 | 11 | // Credentials is a generic implementation of credential storage 12 | // over key-value Storage. 13 | type Credentials struct { 14 | storage Storage 15 | phoneKey, passwordKey string 16 | } 17 | 18 | // NewCredentials creates new Credentials. 19 | func NewCredentials(storage Storage) Credentials { 20 | return Credentials{ 21 | storage: storage, 22 | phoneKey: "phone", 23 | passwordKey: "password", 24 | } 25 | } 26 | 27 | // WithPhoneKey sets phone key to use. 28 | func (c Credentials) WithPhoneKey(phoneKey string) Credentials { 29 | c.phoneKey = phoneKey 30 | return c 31 | } 32 | 33 | // WithPasswordKey sets password key to use. 34 | func (c Credentials) WithPasswordKey(passwordKey string) Credentials { 35 | c.passwordKey = passwordKey 36 | return c 37 | } 38 | 39 | // Phone implements Credentials and returns phone. 40 | func (c Credentials) Phone(ctx context.Context) (string, error) { 41 | return c.storage.Get(ctx, c.phoneKey) 42 | } 43 | 44 | // Password implements Credentials and returns password. 45 | func (c Credentials) Password(ctx context.Context) (string, error) { 46 | r, err := c.storage.Get(ctx, c.passwordKey) 47 | if errors.Is(err, ErrKeyNotFound) { 48 | return r, tgauth.ErrPasswordNotProvided 49 | } 50 | return r, err 51 | } 52 | 53 | // SavePhone stores given phone to storage. 54 | func (c Credentials) SavePhone(ctx context.Context, phone string) error { 55 | return c.storage.Set(ctx, c.phoneKey, phone) 56 | } 57 | 58 | // SavePassword stores given password to storage. 59 | func (c Credentials) SavePassword(ctx context.Context, password string) error { 60 | return c.storage.Set(ctx, c.passwordKey, password) 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | test: 13 | runs-on: ${{ matrix.runner }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | flags: [""] 18 | go: 19 | - 1.24.x 20 | arch: 21 | - amd64 22 | runner: 23 | - ubuntu-latest 24 | - macos-latest 25 | include: 26 | - arch: 386 27 | go: 1.24.x 28 | runner: ubuntu-latest 29 | 30 | - arch: amd64 31 | runner: windows-latest 32 | go: 1.24.x 33 | flags: "-p=1" 34 | 35 | - arch: amd64 36 | go: 1.24.x 37 | runner: ubuntu-latest 38 | flags: "-race" 39 | steps: 40 | - name: Checkout code 41 | uses: actions/checkout@v5.0.0 42 | 43 | - name: Install Go 44 | uses: actions/setup-go@v5.5.0 45 | with: 46 | go-version: ${{ matrix.go }} 47 | cache: false 48 | 49 | - name: Get Go environment 50 | id: go-env 51 | run: | 52 | echo "::set-output name=cache::$(go env GOCACHE)" 53 | echo "::set-output name=modcache::$(go env GOMODCACHE)" 54 | - name: Set up cache 55 | uses: actions/cache@v4.3.0 56 | with: 57 | path: | 58 | ${{ steps.go-env.outputs.cache }} 59 | ${{ steps.go-env.outputs.modcache }} 60 | key: test-${{ runner.os }}-${{ matrix.arch }}-go-${{ matrix.go }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} 61 | restore-keys: | 62 | test-${{ runner.os }}-${{ matrix.arch }}-go-${{ matrix.go }}- 63 | 64 | - name: Run tests 65 | env: 66 | GOARCH: ${{ matrix.arch }} 67 | GOFLAGS: ${{ matrix.flags }} 68 | run: go test --timeout 5m ./... 69 | -------------------------------------------------------------------------------- /bg/connect.go: -------------------------------------------------------------------------------- 1 | // Package bg implements wrapper for running client in background. 2 | package bg 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | ) 8 | 9 | // Client abstracts telegram client. 10 | type Client interface { 11 | Run(ctx context.Context, f func(ctx context.Context) error) error 12 | } 13 | 14 | // StopFunc closes Client and waits until Run returns. 15 | type StopFunc func() error 16 | 17 | type connectOptions struct { 18 | ctx context.Context 19 | } 20 | 21 | // Option for Connect. 22 | type Option interface { 23 | apply(o *connectOptions) 24 | } 25 | 26 | type fnOption func(o *connectOptions) 27 | 28 | func (f fnOption) apply(o *connectOptions) { 29 | f(o) 30 | } 31 | 32 | // WithContext sets base context for client. 33 | func WithContext(ctx context.Context) Option { 34 | return fnOption(func(o *connectOptions) { 35 | o.ctx = ctx 36 | }) 37 | } 38 | 39 | // Connect blocks until client is connected, calling Run internally in 40 | // background. 41 | func Connect(client Client, options ...Option) (StopFunc, error) { 42 | opt := &connectOptions{ 43 | ctx: context.Background(), 44 | } 45 | for _, o := range options { 46 | o.apply(opt) 47 | } 48 | 49 | ctx, cancel := context.WithCancel(opt.ctx) 50 | 51 | errC := make(chan error, 1) 52 | initDone := make(chan struct{}) 53 | go func() { 54 | defer close(errC) 55 | errC <- client.Run(ctx, func(ctx context.Context) error { 56 | close(initDone) 57 | <-ctx.Done() 58 | if errors.Is(ctx.Err(), context.Canceled) { 59 | return nil 60 | } 61 | return ctx.Err() 62 | }) 63 | }() 64 | 65 | select { 66 | case <-ctx.Done(): // context canceled 67 | cancel() 68 | return func() error { return nil }, ctx.Err() 69 | case err := <-errC: // startup timeout 70 | cancel() 71 | return func() error { return nil }, err 72 | case <-initDone: // init done 73 | } 74 | 75 | stopFn := func() error { 76 | cancel() 77 | return <-errC 78 | } 79 | return stopFn, nil 80 | } 81 | -------------------------------------------------------------------------------- /_tools/go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= 2 | github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 3 | golang.org/go-faster/errors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 4 | golang.org/go-faster/errors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= 5 | golang.org/go-faster/errors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 6 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 7 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 8 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 9 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 10 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 11 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 12 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 13 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 14 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 15 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 16 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e h1:aZzprAO9/8oim3qStq3wc1Xuxx4QmAGriC4VU4ojemQ= 17 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 18 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 19 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 20 | -------------------------------------------------------------------------------- /storage/hook_example_test.go: -------------------------------------------------------------------------------- 1 | package storage_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | 9 | pebbledb "github.com/cockroachdb/pebble" 10 | "github.com/go-faster/errors" 11 | 12 | "github.com/gotd/td/telegram" 13 | "github.com/gotd/td/telegram/message" 14 | "github.com/gotd/td/tg" 15 | 16 | "github.com/gotd/contrib/pebble" 17 | "github.com/gotd/contrib/storage" 18 | ) 19 | 20 | func updatesHook(ctx context.Context) error { 21 | db, err := pebbledb.Open("pebble.db", &pebbledb.Options{}) 22 | if err != nil { 23 | return errors.Errorf("create pebble storage: %w", err) 24 | } 25 | s := pebble.NewPeerStorage(db) 26 | 27 | dispatcher := tg.NewUpdateDispatcher() 28 | handler := storage.UpdateHook(dispatcher, s) 29 | client, err := telegram.ClientFromEnvironment(telegram.Options{ 30 | UpdateHandler: handler, 31 | }) 32 | if err != nil { 33 | return errors.Errorf("create client: %w", err) 34 | } 35 | raw := tg.NewClient(client) 36 | sender := message.NewSender(raw) 37 | 38 | dispatcher.OnNewMessage(func(ctx context.Context, e tg.Entities, update *tg.UpdateNewMessage) error { 39 | msg, ok := update.Message.(*tg.Message) 40 | if !ok { 41 | return nil 42 | } 43 | 44 | // Use PeerID to find peer because *Short updates does not contain any entities, so it necessary to 45 | // store some entities. 46 | // Storage can be filled using PeerCollector. 47 | p, err := storage.FindPeer(ctx, s, msg.GetPeerID()) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | _, err = sender.To(p.AsInputPeer()).Text(ctx, msg.GetMessage()) 53 | return err 54 | }) 55 | 56 | return client.Run(ctx, func(ctx context.Context) error { 57 | return telegram.RunUntilCanceled(ctx, client) 58 | }) 59 | } 60 | 61 | func ExampleUpdateHook() { 62 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) 63 | defer cancel() 64 | 65 | if err := updatesHook(ctx); err != nil { 66 | _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) 67 | os.Exit(1) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v5.5.0 16 | with: 17 | go-version: 1.24.x 18 | cache: false 19 | 20 | - name: Get Go environment 21 | id: go-env 22 | run: | 23 | echo "::set-output name=cache::$(go env GOCACHE)" 24 | echo "::set-output name=modcache::$(go env GOMODCACHE)" 25 | - name: Set up cache 26 | uses: actions/cache@v4.3.0 27 | with: 28 | path: | 29 | ${{ steps.go-env.outputs.cache }} 30 | ${{ steps.go-env.outputs.modcache }} 31 | key: test-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 32 | restore-keys: | 33 | test-${{ runner.os }}-go- 34 | 35 | - name: Checkout code 36 | uses: actions/checkout@v5.0.0 37 | 38 | - name: Download dependencies 39 | run: go mod download && go mod tidy 40 | 41 | - name: Run tests with coverage 42 | run: make coverage 43 | 44 | - name: Upload artifact 45 | uses: actions/upload-artifact@v4.6.2 46 | with: 47 | name: coverage 48 | path: profile.out 49 | if-no-files-found: error 50 | retention-days: 1 51 | 52 | - name: Send coverage 53 | uses: codecov/codecov-action@v5.5.1 54 | with: 55 | file: profile.out 56 | 57 | upload: 58 | runs-on: ubuntu-latest 59 | needs: 60 | - test 61 | steps: 62 | - name: Checkout code 63 | uses: actions/checkout@v5.0.0 64 | 65 | - name: Download artifact 66 | uses: actions/download-artifact@v5.0.0 67 | with: 68 | name: coverage 69 | 70 | - name: Send coverage 71 | uses: codecov/codecov-action@v5.5.1 72 | with: 73 | file: profile.out 74 | -------------------------------------------------------------------------------- /middleware/floodwait/scheduler.go: -------------------------------------------------------------------------------- 1 | package floodwait 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "github.com/gotd/td/bin" 9 | "github.com/gotd/td/clock" 10 | "github.com/gotd/td/tg" 11 | ) 12 | 13 | type scheduler struct { 14 | state map[key]time.Duration 15 | mux sync.Mutex 16 | queue *queue 17 | 18 | clock clock.Clock 19 | dec time.Duration 20 | } 21 | 22 | func newScheduler(c clock.Clock, dec time.Duration) *scheduler { 23 | const initialCapacity = 16 24 | 25 | return &scheduler{ 26 | state: make(map[key]time.Duration, initialCapacity), 27 | queue: newQueue(initialCapacity), 28 | clock: c, 29 | dec: dec, 30 | } 31 | } 32 | 33 | func (s *scheduler) new(ctx context.Context, input bin.Encoder, output bin.Decoder, next tg.Invoker) <-chan error { 34 | var k key 35 | k.fromEncoder(input) 36 | r := request{ 37 | ctx: ctx, 38 | input: input, 39 | output: output, 40 | next: next, 41 | key: k, 42 | result: make(chan error, 1), 43 | } 44 | 45 | s.mux.Lock() 46 | defer s.mux.Unlock() 47 | s.schedule(r) 48 | return r.result 49 | } 50 | 51 | // schedule adds request to the queue. 52 | // Assumes the mutex is locked. 53 | func (s *scheduler) schedule(r request) { 54 | k := r.key 55 | 56 | var t time.Time 57 | if state, ok := s.state[k]; ok { 58 | t = s.clock.Now().Add(state) 59 | } else { 60 | t = s.clock.Now() 61 | } 62 | s.queue.add(r, t) 63 | } 64 | 65 | func (s *scheduler) gather(r []scheduled) []scheduled { 66 | return s.queue.gather(s.clock.Now(), r) 67 | } 68 | 69 | func (s *scheduler) nice(k key) { 70 | s.mux.Lock() 71 | if state, ok := s.state[k]; ok && state-s.dec > 0 { 72 | s.state[k] = state - s.dec 73 | } else { 74 | delete(s.state, k) 75 | } 76 | s.mux.Unlock() 77 | } 78 | 79 | func (s *scheduler) flood(req request, d time.Duration) { 80 | k := req.key 81 | 82 | s.mux.Lock() 83 | now := s.clock.Now() 84 | if state, ok := s.state[k]; !ok || state < d { 85 | s.state[k] = d 86 | } 87 | s.queue.add(req, now.Add(d)) 88 | s.mux.Unlock() 89 | 90 | s.queue.move(k, now, d) 91 | } 92 | -------------------------------------------------------------------------------- /storage/resolver_cache.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | 8 | "github.com/gotd/td/telegram/message/peer" 9 | "github.com/gotd/td/tg" 10 | ) 11 | 12 | // ResolverCache is a peer.Resolver cache implemented using peer storage. 13 | type ResolverCache struct { 14 | next peer.Resolver 15 | storage PeerStorage 16 | } 17 | 18 | // NewResolverCache creates new ResolverCache. 19 | func NewResolverCache(next peer.Resolver, storage PeerStorage) ResolverCache { 20 | return ResolverCache{next: next, storage: storage} 21 | } 22 | 23 | func (r ResolverCache) notFound( 24 | ctx context.Context, 25 | key string, 26 | f func(context.Context, string) (tg.InputPeerClass, error), 27 | ) (_ tg.InputPeerClass, rerr error) { 28 | // If key not found, try to resolve. 29 | resolved, err := f(ctx, key) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | var value Peer 35 | if err := value.FromInputPeer(resolved); err != nil { 36 | return nil, errors.Errorf("extract object: %w", err) 37 | } 38 | 39 | if err := r.storage.Assign(ctx, key, value); err != nil { 40 | return nil, errors.Errorf("assign %q: %w", key, err) 41 | } 42 | 43 | return resolved, nil 44 | } 45 | 46 | func (r ResolverCache) tryResolve( 47 | ctx context.Context, 48 | key string, 49 | f func(context.Context, string) (tg.InputPeerClass, error), 50 | ) (tg.InputPeerClass, error) { 51 | b, err := r.storage.Resolve(ctx, key) 52 | if err != nil { 53 | if errors.Is(err, ErrPeerNotFound) { 54 | return r.notFound(ctx, key, f) 55 | } 56 | return nil, errors.Errorf("get %q: %w", key, err) 57 | } 58 | return b.AsInputPeer(), nil 59 | } 60 | 61 | // ResolveDomain implements peer.Resolver 62 | func (r ResolverCache) ResolveDomain(ctx context.Context, domain string) (tg.InputPeerClass, error) { 63 | return r.tryResolve(ctx, domain, r.next.ResolveDomain) 64 | } 65 | 66 | // ResolvePhone implements peer.Resolver 67 | func (r ResolverCache) ResolvePhone(ctx context.Context, phone string) (tg.InputPeerClass, error) { 68 | return r.tryResolve(ctx, phone, r.next.ResolvePhone) 69 | } 70 | -------------------------------------------------------------------------------- /storage/key.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/go-faster/errors" 9 | 10 | "github.com/gotd/td/telegram/query/dialogs" 11 | ) 12 | 13 | // PeerKeyPrefix is a key prefix of peer key. 14 | var PeerKeyPrefix = []byte("peer") // nolint:gochecknoglobals 15 | 16 | // PeerKey is unique key of peer object. 17 | type PeerKey struct { 18 | Kind dialogs.PeerKind 19 | ID int64 20 | } 21 | 22 | // KeyFromPeer creates key from peer. 23 | func KeyFromPeer(p Peer) PeerKey { 24 | return PeerKey{ 25 | Kind: p.Key.Kind, 26 | ID: p.Key.ID, 27 | } 28 | } 29 | 30 | const keySeparator = '_' 31 | 32 | // Bytes returns bytes representation of key. 33 | func (k PeerKey) Bytes(r []byte) []byte { 34 | r = append(r, PeerKeyPrefix...) 35 | r = strconv.AppendInt(r, int64(k.Kind), 10) 36 | r = append(r, keySeparator) 37 | r = strconv.AppendInt(r, k.ID, 10) 38 | return r 39 | } 40 | 41 | // String returns string representation of key. 42 | func (k PeerKey) String() string { 43 | var ( 44 | b strings.Builder 45 | buf [64]byte 46 | ) 47 | b.Write(PeerKeyPrefix) 48 | b.Write(strconv.AppendInt(buf[:0], int64(k.Kind), 10)) 49 | b.WriteRune(keySeparator) 50 | b.Write(strconv.AppendInt(buf[:0], k.ID, 10)) 51 | return b.String() 52 | } 53 | 54 | var errInvalidKey = errors.New("invalid key") // nolint:gochecknoglobals 55 | 56 | // Parse parses bytes representation from given slice. 57 | func (k *PeerKey) Parse(r []byte) error { 58 | if !bytes.HasPrefix(r, PeerKeyPrefix) { 59 | return errInvalidKey 60 | } 61 | r = r[len(PeerKeyPrefix):] 62 | 63 | idx := bytes.IndexByte(r, keySeparator) 64 | // Check that slice contains _ and it's not a first or last character. 65 | if idx <= 0 || idx == len(r)-1 { 66 | return errInvalidKey 67 | } 68 | 69 | { 70 | v, err := strconv.Atoi(string(r[:idx])) 71 | if err != nil { 72 | return errors.Errorf("parse kind: %w", err) 73 | } 74 | if v > int(dialogs.Channel) { 75 | return errors.Errorf("invalid kind %d", v) 76 | } 77 | k.Kind = dialogs.PeerKind(v) 78 | } 79 | 80 | { 81 | v, err := strconv.ParseInt(string(r[idx+1:]), 10, 64) 82 | if err != nil { 83 | return errors.Errorf("parse id: %w", err) 84 | } 85 | k.ID = v 86 | } 87 | 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /http_range/range_test.go: -------------------------------------------------------------------------------- 1 | package http_range 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParseRange(t *testing.T) { 9 | type args struct { 10 | s string 11 | size int64 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want []Range 17 | wantErr bool 18 | }{ 19 | { 20 | name: "blank", 21 | }, 22 | { 23 | name: "invalid", 24 | args: args{ 25 | s: "keks=100500", 26 | size: 100, 27 | }, 28 | wantErr: true, 29 | }, 30 | { 31 | name: "invalid single value", 32 | args: args{ 33 | s: "bytes=200", 34 | size: 500, 35 | }, 36 | wantErr: true, 37 | }, 38 | { 39 | name: "invalid non-digit end", 40 | args: args{ 41 | s: "bytes=-f", 42 | size: 500, 43 | }, 44 | wantErr: true, 45 | }, 46 | { 47 | name: "invalid no start or end", 48 | args: args{ 49 | s: "bytes=-", 50 | size: 500, 51 | }, 52 | wantErr: true, 53 | }, 54 | { 55 | name: "invalid non-digit start", 56 | args: args{ 57 | s: "bytes=f-", 58 | size: 500, 59 | }, 60 | wantErr: true, 61 | }, 62 | { 63 | name: "single", 64 | args: args{ 65 | s: "bytes=100-200", size: 200, 66 | }, 67 | want: []Range{ 68 | { 69 | Start: 100, 70 | Length: 100, 71 | }, 72 | }, 73 | }, 74 | { 75 | name: "no overlap", 76 | args: args{ 77 | s: "bytes=100-50", size: 200, 78 | }, 79 | wantErr: true, 80 | }, 81 | { 82 | name: "after end", 83 | args: args{ 84 | s: "bytes=200-250", size: 200, 85 | }, 86 | wantErr: true, 87 | }, 88 | { 89 | name: "from offset till end", 90 | args: args{ 91 | s: "bytes=50-", size: 200, 92 | }, 93 | want: []Range{ 94 | { 95 | Start: 50, 96 | Length: 150, 97 | }, 98 | }, 99 | }, 100 | } 101 | for _, tt := range tests { 102 | t.Run(tt.name, func(t *testing.T) { 103 | got, err := ParseRange(tt.args.s, tt.args.size) 104 | if (err != nil) != tt.wantErr { 105 | t.Errorf("ParseRange() error = %v, wantErr %v", err, tt.wantErr) 106 | return 107 | } 108 | if !reflect.DeepEqual(got, tt.want) { 109 | t.Errorf("ParseRange() got = %v, want %v", got, tt.want) 110 | } 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /storage/collector.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | 8 | "github.com/gotd/td/telegram/query/channels/participants" 9 | "github.com/gotd/td/telegram/query/dialogs" 10 | "github.com/gotd/td/tg" 11 | ) 12 | 13 | // PeerCollector is a simple helper to collect peers from different sources. 14 | type PeerCollector struct { 15 | storage PeerStorage 16 | } 17 | 18 | // Dialogs collects peers from dialog iterator. 19 | func (c PeerCollector) Dialogs(ctx context.Context, iter *dialogs.Iterator) error { 20 | for iter.Next(ctx) { 21 | var ( 22 | p Peer 23 | value = iter.Value() 24 | ) 25 | switch dlg := value.Dialog.GetPeer().(type) { 26 | case *tg.PeerUser: 27 | user, ok := value.Entities.User(dlg.UserID) 28 | if !ok || !p.FromUser(user) { 29 | continue 30 | } 31 | case *tg.PeerChat: 32 | chat, ok := value.Entities.Chat(dlg.ChatID) 33 | if !ok || !p.FromChat(chat) { 34 | continue 35 | } 36 | case *tg.PeerChannel: 37 | channel, ok := value.Entities.Channel(dlg.ChannelID) 38 | if !ok || !p.FromChat(channel) { 39 | continue 40 | } 41 | } 42 | 43 | if err := c.storage.Add(ctx, p); err != nil { 44 | return errors.Errorf("add: %w", err) 45 | } 46 | } 47 | 48 | return iter.Err() 49 | } 50 | 51 | // Participants collects peers from participants iterator. 52 | func (c PeerCollector) Participants(ctx context.Context, iter *participants.Iterator) error { 53 | for iter.Next(ctx) { 54 | var ( 55 | p Peer 56 | value = iter.Value() 57 | ) 58 | user, ok := value.User() 59 | if !ok { 60 | continue 61 | } 62 | 63 | if !p.FromUser(user) { 64 | continue 65 | } 66 | if err := c.storage.Add(ctx, p); err != nil { 67 | return errors.Errorf("add: %w", err) 68 | } 69 | } 70 | 71 | return iter.Err() 72 | } 73 | 74 | // Contacts collects peers from contacts iterator. 75 | func (c PeerCollector) Contacts(ctx context.Context, contacts *tg.ContactsContacts) error { 76 | for _, user := range contacts.Users { 77 | var p Peer 78 | if !p.FromUser(user) { 79 | continue 80 | } 81 | if err := c.storage.Add(ctx, p); err != nil { 82 | return errors.Errorf("add: %w", err) 83 | } 84 | } 85 | return nil 86 | } 87 | 88 | // CollectPeers creates new PeerCollector. 89 | func CollectPeers(storage PeerStorage) PeerCollector { 90 | return PeerCollector{storage: storage} 91 | } 92 | -------------------------------------------------------------------------------- /middleware/floodwait/queue.go: -------------------------------------------------------------------------------- 1 | package floodwait 2 | 3 | import ( 4 | "container/heap" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type scheduled struct { 10 | request request 11 | sendTime time.Time 12 | index int 13 | } 14 | 15 | type scheduledHeap []scheduled 16 | 17 | func (r scheduledHeap) Len() int { return len(r) } 18 | 19 | func (r scheduledHeap) Less(i, j int) bool { 20 | return r[i].sendTime.UnixNano() < r[j].sendTime.UnixNano() 21 | } 22 | 23 | func (r scheduledHeap) Swap(i, j int) { 24 | r[i], r[j] = r[j], r[i] 25 | r[i].index = i 26 | r[j].index = j 27 | } 28 | 29 | func (r *scheduledHeap) Push(x interface{}) { 30 | n := len(*r) 31 | item := x.(scheduled) 32 | item.index = n 33 | *r = append(*r, item) 34 | } 35 | 36 | func (r *scheduledHeap) Pop() interface{} { 37 | old := *r 38 | n := len(old) 39 | item := old[n-1] 40 | item.index = -1 // for safety 41 | *r = old[0 : n-1] 42 | return item 43 | } 44 | 45 | type queue struct { 46 | requests scheduledHeap 47 | requestsMux sync.Mutex 48 | } 49 | 50 | func newQueue(initialCapacity int) *queue { 51 | r := make(scheduledHeap, 0, initialCapacity) 52 | return &queue{requests: r} 53 | } 54 | 55 | func (q *queue) add(r request, t time.Time) { 56 | q.requestsMux.Lock() 57 | defer q.requestsMux.Unlock() 58 | 59 | heap.Push(&q.requests, scheduled{ 60 | request: r, 61 | sendTime: t, 62 | }) 63 | } 64 | 65 | func (q *queue) len() int { 66 | q.requestsMux.Lock() 67 | r := len(q.requests) 68 | q.requestsMux.Unlock() 69 | return r 70 | } 71 | 72 | func (q *queue) move(k key, now time.Time, dur time.Duration) { 73 | q.requestsMux.Lock() 74 | defer q.requestsMux.Unlock() 75 | 76 | for idx, s := range q.requests { 77 | if s.request.key != k { 78 | continue 79 | } 80 | 81 | t := s.sendTime 82 | if t.Sub(now) > dur { 83 | break 84 | } 85 | q.requests[idx].sendTime = t.Add(dur) 86 | } 87 | heap.Init(&q.requests) 88 | } 89 | 90 | func (q *queue) gather(now time.Time, req []scheduled) []scheduled { 91 | q.requestsMux.Lock() 92 | defer q.requestsMux.Unlock() 93 | 94 | for { 95 | if q.requests.Len() < 1 { 96 | return req 97 | } 98 | 99 | next := heap.Pop(&q.requests).(scheduled) 100 | if now.Before(next.sendTime) { 101 | heap.Push(&q.requests, next) 102 | return req 103 | } 104 | req = append(req, next) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /middleware/ratelimit/ratelimit.go: -------------------------------------------------------------------------------- 1 | package ratelimit 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "golang.org/x/time/rate" 9 | 10 | "github.com/gotd/td/bin" 11 | "github.com/gotd/td/clock" 12 | "github.com/gotd/td/telegram" 13 | "github.com/gotd/td/tg" 14 | ) 15 | 16 | // RateLimiter is a tg.Invoker that throttles RPC calls on underlying invoker. 17 | type RateLimiter struct { 18 | clock clock.Clock 19 | lim *rate.Limiter 20 | } 21 | 22 | // New returns a new invoker rate limiter using lim. 23 | func New(r rate.Limit, b int) *RateLimiter { 24 | return &RateLimiter{ 25 | clock: clock.System, 26 | lim: rate.NewLimiter(r, b), 27 | } 28 | } 29 | 30 | // clone returns a copy of the RateLimiter. 31 | func (l *RateLimiter) clone() *RateLimiter { 32 | return &RateLimiter{ 33 | clock: l.clock, 34 | lim: l.lim, 35 | } 36 | } 37 | 38 | // WithClock sets clock to use. Default is to use system clock. 39 | func (l *RateLimiter) WithClock(c clock.Clock) *RateLimiter { 40 | l = l.clone() 41 | l.clock = c 42 | return l 43 | } 44 | 45 | // wait blocks until rate limiter permits an event to happen. It returns an error if 46 | // limiter’s burst size is misconfigured, the Context is canceled, or the expected 47 | // wait time exceeds the Context’s Deadline. 48 | func (l *RateLimiter) wait(ctx context.Context) error { 49 | // Check if ctx is already canceled. 50 | select { 51 | case <-ctx.Done(): 52 | return ctx.Err() 53 | default: 54 | } 55 | 56 | now := l.clock.Now() 57 | 58 | r := l.lim.ReserveN(now, 1) 59 | if !r.OK() { 60 | // Limiter requires n <= lim.burst for each reservation. 61 | return errors.New("limiter's burst size must be greater than zero") 62 | } 63 | 64 | delay := r.DelayFrom(now) 65 | if delay == 0 { 66 | return nil 67 | } 68 | 69 | // Bail out earlier if we exceed context deadline. Note that 70 | // contexts use system time instead of mockable clock. 71 | deadline, ok := ctx.Deadline() 72 | if ok && delay > time.Until(deadline) { 73 | return context.DeadlineExceeded 74 | } 75 | 76 | t := l.clock.Timer(delay) 77 | defer clock.StopTimer(t) 78 | select { 79 | case <-t.C(): 80 | return nil 81 | case <-ctx.Done(): 82 | r.CancelAt(l.clock.Now()) 83 | return ctx.Err() 84 | } 85 | } 86 | 87 | // Handle implements telegram.Middleware. 88 | func (l *RateLimiter) Handle(next tg.Invoker) telegram.InvokeFunc { 89 | return func(ctx context.Context, input bin.Encoder, output bin.Decoder) error { 90 | if err := l.wait(ctx); err != nil { 91 | return err 92 | } 93 | return next.Invoke(ctx, input, output) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /vault/client.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "path" 6 | 7 | "github.com/go-faster/errors" 8 | "github.com/hashicorp/vault/api" 9 | "go.uber.org/multierr" 10 | 11 | "github.com/gotd/contrib/auth/kv" 12 | ) 13 | 14 | type vaultClient struct { 15 | client *api.Client 16 | path string 17 | } 18 | 19 | func (c vaultClient) Set(ctx context.Context, k, v string) error { 20 | return c.add(ctx, k, v) 21 | } 22 | 23 | func (c vaultClient) Get(ctx context.Context, k string) (string, error) { 24 | return c.get(ctx, k) 25 | } 26 | 27 | func (c vaultClient) getPath() string { 28 | return path.Join("/v1/", c.path) 29 | } 30 | 31 | func (c vaultClient) putAll(ctx context.Context, data map[string]interface{}) error { 32 | req := c.client.NewRequest("PUT", c.getPath()) 33 | 34 | err := req.SetJSONBody(data) 35 | if err != nil { 36 | return errors.Errorf("request encode: %w", err) 37 | } 38 | 39 | resp, err := c.client.RawRequestWithContext(ctx, req) 40 | if resp != nil { 41 | defer func() { 42 | multierr.AppendInto(&err, resp.Body.Close()) 43 | }() 44 | } 45 | if err != nil { 46 | return errors.Errorf("secret send: %w", err) 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (c vaultClient) add(ctx context.Context, key, value string) error { 53 | s, err := c.getAll(ctx) 54 | data := map[string]interface{}{} 55 | if err != nil && !errors.Is(err, kv.ErrKeyNotFound) { 56 | return err 57 | } 58 | if err == nil { 59 | data = s.Data 60 | } 61 | 62 | data[key] = value 63 | return c.putAll(ctx, data) 64 | } 65 | 66 | func (c vaultClient) getAll(ctx context.Context) (*api.Secret, error) { 67 | req := c.client.NewRequest("GET", c.getPath()) 68 | 69 | resp, err := c.client.RawRequestWithContext(ctx, req) 70 | if resp != nil { 71 | defer func() { 72 | multierr.AppendInto(&err, resp.Body.Close()) 73 | }() 74 | 75 | if resp.StatusCode == 404 { 76 | return nil, kv.ErrKeyNotFound 77 | } 78 | } 79 | if err != nil { 80 | return nil, errors.Errorf("secret fetch: %w", err) 81 | } 82 | 83 | secret, err := api.ParseSecret(resp.Body) 84 | if err != nil { 85 | return nil, errors.Errorf("secret parsing: %w", err) 86 | } 87 | 88 | return secret, nil 89 | } 90 | 91 | func (c vaultClient) get(ctx context.Context, key string) (string, error) { 92 | secret, err := c.getAll(ctx) 93 | if err != nil { 94 | return "", err 95 | } 96 | 97 | data, ok := secret.Data[key] 98 | if !ok { 99 | return "", kv.ErrKeyNotFound 100 | } 101 | 102 | session, ok := data.(string) 103 | if !ok { 104 | return "", errors.Errorf("expected %q have string type, got %T", key, data) 105 | } 106 | 107 | return session, nil 108 | } 109 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | golangci-lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v5.0.0 17 | 18 | - name: Install Go 19 | uses: actions/setup-go@v5.5.0 20 | with: 21 | go-version: 1.21.x 22 | cache: false 23 | 24 | - name: Lint 25 | uses: golangci/golangci-lint-action@v8.0.0 26 | with: 27 | version: latest 28 | args: --timeout 5m 29 | 30 | # Check if there are any dirty changes after go mod tidy 31 | check-mod: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Checkout code 35 | uses: actions/checkout@v5.0.0 36 | 37 | - name: Install Go 38 | uses: actions/setup-go@v5.5.0 39 | with: 40 | go-version: 1.21.x 41 | cache: false 42 | 43 | - name: Get Go environment 44 | id: go-env 45 | run: | 46 | echo "::set-output name=cache::$(go env GOCACHE)" 47 | echo "::set-output name=modcache::$(go env GOMODCACHE)" 48 | - name: Set up cache 49 | uses: actions/cache@v4.3.0 50 | with: 51 | path: | 52 | ${{ steps.go-env.outputs.cache }} 53 | ${{ steps.go-env.outputs.modcache }} 54 | key: check-mod-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 55 | restore-keys: | 56 | check-mod-${{ runner.os }}-go- 57 | 58 | - name: Download dependencies 59 | run: go mod download && go mod tidy 60 | 61 | - name: Check git diff 62 | run: git diff --exit-code 63 | 64 | # Check if there are any dirty changes after go generate 65 | check-generate: 66 | runs-on: ubuntu-latest 67 | steps: 68 | - name: Checkout code 69 | uses: actions/checkout@v5.0.0 70 | 71 | - name: Install Go 72 | uses: actions/setup-go@v5.5.0 73 | with: 74 | go-version: 1.21.x 75 | cache: false 76 | 77 | - name: Get Go environment 78 | id: go-env 79 | run: | 80 | echo "::set-output name=cache::$(go env GOCACHE)" 81 | echo "::set-output name=modcache::$(go env GOMODCACHE)" 82 | - name: Set up cache 83 | uses: actions/cache@v4.3.0 84 | with: 85 | path: | 86 | ${{ steps.go-env.outputs.cache }} 87 | ${{ steps.go-env.outputs.modcache }} 88 | key: check-generate-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 89 | restore-keys: | 90 | check-generate-${{ runner.os }}-go- 91 | 92 | - name: Download dependencies 93 | run: go mod download && go mod tidy 94 | 95 | - name: Generate files 96 | run: make generate 97 | 98 | - name: Check git diff 99 | run: git diff --exit-code 100 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - dogsled 6 | - dupl 7 | - errcheck 8 | - gochecknoglobals 9 | - gochecknoinits 10 | - goconst 11 | - gocritic 12 | - gosec 13 | - govet 14 | - ineffassign 15 | - lll 16 | - misspell 17 | - nakedret 18 | - revive 19 | - staticcheck 20 | - unconvert 21 | - unparam 22 | - unused 23 | - whitespace 24 | settings: 25 | dupl: 26 | threshold: 120 27 | goconst: 28 | min-len: 2 29 | min-occurrences: 3 30 | gocritic: 31 | disabled-checks: 32 | - hugeParam 33 | - rangeValCopy 34 | - exitAfterDefer 35 | - whyNoLint 36 | - singleCaseSwitch 37 | - commentedOutCode 38 | enabled-tags: 39 | - diagnostic 40 | - experimental 41 | - opinionated 42 | - performance 43 | - style 44 | gocyclo: 45 | min-complexity: 15 46 | lll: 47 | line-length: 140 48 | misspell: 49 | locale: US 50 | revive: 51 | rules: 52 | - name: unused-parameter 53 | severity: warning 54 | disabled: true 55 | exclusions: 56 | generated: lax 57 | rules: 58 | - linters: 59 | - gocritic 60 | text: commentedOutCode 61 | source: SHA1 62 | - linters: 63 | - gochecknoglobals 64 | source: embed\.FS 65 | - linters: 66 | - lll 67 | source: //go:generate 68 | - linters: 69 | - dupl 70 | - errcheck 71 | - funlen 72 | - gochecknoglobals 73 | - gocognit 74 | - goconst 75 | - gocyclo 76 | - gosec 77 | - lll 78 | - scopelint 79 | path: _test\.go 80 | - linters: 81 | - govet 82 | text: declaration of "(err|ctx|log)" 83 | - linters: 84 | - golint 85 | path: internal\.go 86 | text: should have.+comment 87 | - linters: 88 | - golint 89 | - staticcheck 90 | text: underscores? in package names? 91 | - linters: 92 | - staticcheck 93 | text: 'SA1019: (telegram|client).+ is deprecated:' 94 | - linters: 95 | - revive 96 | text: 'var-naming: don''t use an underscore in package name' 97 | - linters: 98 | - gosec 99 | text: 'G115: integer overflow conversion' 100 | - linters: 101 | - staticcheck 102 | path: vault/client 103 | text: deprecated 104 | paths: 105 | - third_party$ 106 | - builtin$ 107 | - examples$ 108 | formatters: 109 | enable: 110 | - gofmt 111 | - goimports 112 | settings: 113 | goimports: 114 | local-prefixes: 115 | - github.com/gotd/ 116 | exclusions: 117 | generated: lax 118 | paths: 119 | - third_party$ 120 | - builtin$ 121 | - examples$ 122 | -------------------------------------------------------------------------------- /invoker/update_test.go: -------------------------------------------------------------------------------- 1 | package invoker 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/gotd/td/telegram" 11 | 12 | "github.com/gotd/td/bin" 13 | "github.com/gotd/td/tg" 14 | ) 15 | 16 | func TestUpdateHook_InvokeRaw(t *testing.T) { 17 | t.Run("Success", func(t *testing.T) { 18 | var invokerCalled, hookCalled bool 19 | assert.NoError(t, UpdateHook(func(ctx context.Context, u tg.UpdatesClass) error { 20 | assert.NotNil(t, u) 21 | hookCalled = true 22 | return nil 23 | }).Handle(telegram.InvokeFunc(func(ctx context.Context, input bin.Encoder, output bin.Decoder) error { 24 | invokerCalled = true 25 | return nil 26 | })).Invoke(context.TODO(), nil, &tg.UpdatesBox{ 27 | Updates: &tg.UpdateShortMessage{ 28 | ID: 100, 29 | }, 30 | })) 31 | 32 | assert.True(t, invokerCalled, "invoker should be called") 33 | assert.True(t, hookCalled, "hook should be called") 34 | }) 35 | t.Run("Error", func(t *testing.T) { 36 | t.Run("Handler", func(t *testing.T) { 37 | var invokerCalled, hookCalled bool 38 | err := errors.New("failure") 39 | assert.ErrorIs(t, UpdateHook(func(ctx context.Context, u tg.UpdatesClass) error { 40 | assert.NotNil(t, u) 41 | hookCalled = true 42 | return nil 43 | }).Handle(telegram.InvokeFunc(func(ctx context.Context, input bin.Encoder, output bin.Decoder) error { 44 | invokerCalled = true 45 | return err 46 | })).Invoke(context.TODO(), nil, &tg.UpdatesBox{ 47 | Updates: &tg.UpdateShortMessage{ 48 | ID: 100, 49 | }, 50 | }), err) 51 | 52 | assert.True(t, invokerCalled, "invoker should be called") 53 | assert.False(t, hookCalled, "hook should not be called") 54 | }) 55 | t.Run("Hook", func(t *testing.T) { 56 | var invokerCalled, hookCalled bool 57 | err := errors.New("failure") 58 | assert.ErrorIs(t, UpdateHook(func(ctx context.Context, u tg.UpdatesClass) error { 59 | assert.NotNil(t, u) 60 | hookCalled = true 61 | return err 62 | }).Handle(telegram.InvokeFunc(func(ctx context.Context, input bin.Encoder, output bin.Decoder) error { 63 | invokerCalled = true 64 | return nil 65 | })).Invoke(context.TODO(), nil, &tg.UpdatesBox{ 66 | Updates: &tg.UpdateShortMessage{ 67 | ID: 100, 68 | }, 69 | }), err) 70 | 71 | assert.True(t, invokerCalled, "invoker should be called") 72 | assert.True(t, hookCalled, "hook should be called") 73 | }) 74 | }) 75 | t.Run("Not update", func(t *testing.T) { 76 | var invokerCalled, hookCalled bool 77 | assert.NoError(t, UpdateHook(func(ctx context.Context, u tg.UpdatesClass) error { 78 | assert.NotNil(t, u) 79 | hookCalled = true 80 | return nil 81 | }).Handle(telegram.InvokeFunc(func(ctx context.Context, input bin.Encoder, output bin.Decoder) error { 82 | invokerCalled = true 83 | return nil 84 | })).Invoke(context.TODO(), nil, &tg.User{})) 85 | 86 | assert.True(t, invokerCalled, "invoker should be called") 87 | assert.False(t, hookCalled, "hook should not be called") 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /storage/peer_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/go-faster/jx" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/gotd/td/telegram/query/dialogs" 14 | "github.com/gotd/td/tg" 15 | ) 16 | 17 | func TestPeer_MarshalJSON(t *testing.T) { 18 | t.Run("Latest", func(t *testing.T) { 19 | // Prepare data. 20 | chat := &tg.Chat{ 21 | Photo: &tg.ChatPhotoEmpty{}, 22 | } 23 | chat.SetFlags() 24 | 25 | user := &tg.User{ 26 | Username: "foo", 27 | ID: 100, 28 | AccessHash: 200, 29 | Photo: &tg.UserProfilePhotoEmpty{}, 30 | } 31 | user.SetFlags() 32 | 33 | channel := &tg.Channel{ 34 | Photo: &tg.ChatPhotoEmpty{}, 35 | ParticipantsCount: 200, 36 | } 37 | channel.SetFlags() 38 | 39 | key := dialogs.DialogKey{ 40 | ID: 1, 41 | AccessHash: 3, 42 | Kind: dialogs.User, 43 | } 44 | meta := map[string]any{ 45 | "foo": "bar", 46 | "v": true, 47 | } 48 | 49 | for i, p := range []Peer{ 50 | { 51 | Version: LatestVersion, 52 | Key: key, 53 | User: user, 54 | Chat: chat, 55 | Channel: channel, 56 | Metadata: meta, 57 | CreatedAt: time.Unix(1681541743, 0), 58 | }, 59 | { 60 | Version: LatestVersion, 61 | Key: key, 62 | CreatedAt: time.Unix(1682525712, 0), 63 | }, 64 | } { 65 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 66 | { 67 | // Just print. 68 | var e jx.Encoder 69 | e.SetIdent(2) 70 | require.NoError(t, p.Marshal(&e)) 71 | t.Log(e.String()) 72 | } 73 | 74 | data, err := json.Marshal(p) 75 | require.NoError(t, err) 76 | 77 | var out Peer 78 | require.NoError(t, json.Unmarshal(data, &out)) 79 | 80 | assert.Equal(t, p.CreatedAt, out.CreatedAt, "CreatedAt") 81 | assert.Equal(t, p.Version, out.Version, "Version") 82 | assert.Equal(t, p.Key, out.Key, "Key") 83 | assert.Equal(t, p.Metadata, out.Metadata, "Metadata") 84 | if assert.True(t, (p.User == nil) == (out.User == nil), "User nil") && p.User != nil { 85 | assert.Equal(t, *p.User, *out.User, "User") 86 | } 87 | if assert.True(t, (p.Chat == nil) == (out.Chat == nil), "Chat nil") && p.Chat != nil { 88 | assert.Equal(t, *p.Chat, *out.Chat, "Chat") 89 | } 90 | if assert.True(t, (p.Channel == nil) == (out.Channel == nil), "Channel nil") && p.Channel != nil { 91 | assert.Equal(t, *p.Channel, *out.Channel, "Channel") 92 | } 93 | }) 94 | } 95 | }) 96 | t.Run("Outdated", func(t *testing.T) { 97 | var d jx.Encoder 98 | d.SetIdent(2) 99 | d.Obj(func(e *jx.Encoder) { 100 | e.Field("Version", func(e *jx.Encoder) { 101 | e.Int(1) 102 | }) 103 | }) 104 | err := json.Unmarshal(d.Bytes(), &Peer{}) 105 | require.Error(t, err) 106 | require.ErrorIs(t, err, ErrPeerUnmarshalMustInvalidate) 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /middleware/tg_prom/middleware.go: -------------------------------------------------------------------------------- 1 | // Package tg_prom implements middleware for prometheus metrics. 2 | package tg_prom 3 | 4 | import ( 5 | "context" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/prometheus/client_golang/prometheus" 10 | 11 | "github.com/gotd/td/bin" 12 | "github.com/gotd/td/telegram" 13 | "github.com/gotd/td/tg" 14 | "github.com/gotd/td/tgerr" 15 | ) 16 | 17 | // Middleware is prometheus metrics middleware for Telegram. 18 | type Middleware struct { 19 | count *prometheus.CounterVec 20 | failures *prometheus.CounterVec 21 | duration prometheus.ObserverVec 22 | } 23 | 24 | // Metrics returns slice of provided prometheus metrics. 25 | func (m Middleware) Metrics() []prometheus.Collector { 26 | return []prometheus.Collector{ 27 | m.count, 28 | m.failures, 29 | m.duration, 30 | } 31 | } 32 | 33 | const ( 34 | labelErrType = "tg_err_type" 35 | labelErrCode = "tg_err_code" 36 | labelMethod = "tg_method" 37 | ) 38 | 39 | // Handle implements telegram.Middleware. 40 | func (m Middleware) Handle(next tg.Invoker) telegram.InvokeFunc { 41 | return func(ctx context.Context, input bin.Encoder, output bin.Decoder) error { 42 | // Prepare. 43 | labels := m.labels(input) 44 | m.count.With(labels).Inc() 45 | start := time.Now() 46 | 47 | // Call actual method. 48 | err := next.Invoke(ctx, input, output) 49 | 50 | // Observe. 51 | m.duration.With(labels).Observe(time.Since(start).Seconds()) 52 | if err != nil { 53 | failureLabels := prometheus.Labels{} 54 | for k, v := range labels { 55 | failureLabels[k] = v 56 | } 57 | if rpcErr, ok := tgerr.As(err); ok { 58 | failureLabels[labelErrType] = rpcErr.Type 59 | failureLabels[labelErrCode] = strconv.Itoa(rpcErr.Code) 60 | } else { 61 | failureLabels[labelErrType] = "CLIENT" 62 | } 63 | m.failures.With(failureLabels) 64 | } 65 | 66 | return err 67 | } 68 | } 69 | 70 | // object is a abstraction for Telegram API object with TypeName. 71 | type object interface { 72 | TypeName() string 73 | } 74 | 75 | func (m Middleware) labels(input bin.Encoder) prometheus.Labels { 76 | obj, ok := input.(object) 77 | if !ok { 78 | return prometheus.Labels{} 79 | } 80 | return prometheus.Labels{ 81 | labelMethod: obj.TypeName(), 82 | } 83 | } 84 | 85 | // New initializes and returns new prometheus middleware. 86 | func New() *Middleware { 87 | return &Middleware{ 88 | count: prometheus.NewCounterVec(prometheus.CounterOpts{ 89 | Name: "tg_rpc_count_total", 90 | Help: "Telegram RPC calls total count.", 91 | }, []string{labelMethod}), 92 | duration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ 93 | Name: "tg_rpc_duration_seconds", 94 | Help: "Telegram RPC calls duration histogram.", 95 | }, []string{labelMethod}), 96 | failures: prometheus.NewCounterVec(prometheus.CounterOpts{ 97 | Name: "tg_rpc_failures_total", 98 | Help: "Telegram failed RPC calls total count.", 99 | }, []string{labelMethod, labelErrCode, labelErrType}), 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /http_io/io.go: -------------------------------------------------------------------------------- 1 | // Package http_io implements http handlers based on partial input/output primitives. 2 | package http_io 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "strconv" 11 | 12 | "go.uber.org/zap" 13 | 14 | "github.com/gotd/contrib/http_range" 15 | ) 16 | 17 | // StreamerAt implements streaming with offset. 18 | type StreamerAt interface { 19 | StreamAt(ctx context.Context, skip int64, w io.Writer) error 20 | } 21 | 22 | // Handler implements ranged http requests on top of StreamerAt interface. 23 | type Handler struct { 24 | log *zap.Logger 25 | size int64 26 | contentType string 27 | streamer StreamerAt 28 | } 29 | 30 | // WithLog sets logger of handler. 31 | func (h *Handler) WithLog(log *zap.Logger) *Handler { 32 | h.log = log 33 | return h 34 | } 35 | 36 | // WithContentType sets contentType header. 37 | func (h *Handler) WithContentType(contentType string) *Handler { 38 | h.contentType = contentType 39 | return h 40 | } 41 | 42 | // ServeHTTP implements http.Handler. 43 | func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 44 | ranges, err := http_range.ParseRange(r.Header.Get("Range"), h.size) 45 | if err == http_range.ErrNoOverlap { 46 | w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", h.size)) 47 | http.Error(w, http_range.ErrNoOverlap.Error(), http.StatusRequestedRangeNotSatisfiable) 48 | return 49 | } 50 | if err != nil { 51 | http.Error(w, err.Error(), http.StatusBadRequest) 52 | return 53 | } 54 | if len(ranges) > 1 { 55 | http.Error(w, "multiple ranges are not supported", http.StatusRequestedRangeNotSatisfiable) 56 | return 57 | } 58 | 59 | // Preparing response. 60 | code := http.StatusOK 61 | sendSize := h.size 62 | 63 | var offset int64 64 | if len(ranges) > 0 { 65 | r := ranges[0] 66 | offset = r.Start 67 | sendSize = r.Length 68 | code = http.StatusPartialContent 69 | w.Header().Set("Content-Range", r.ContentRange(h.size)) 70 | } 71 | if h.contentType != "" { 72 | w.Header().Set("Content-Type", h.contentType) 73 | } 74 | w.Header().Set("Accept-Ranges", "bytes") 75 | if w.Header().Get("Content-Encoding") == "" { 76 | w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10)) 77 | } 78 | 79 | // Writing response. 80 | w.WriteHeader(code) 81 | if r.Method == http.MethodHead { 82 | // Not writing body on HEAD. 83 | return 84 | } 85 | h.log.Info("Serving", zap.Int64("offset", offset)) 86 | // TODO: handle case of partial writes (e.g. not until the end of file). 87 | if err := h.streamer.StreamAt(r.Context(), offset, w); err != nil && !errors.Is(err, context.Canceled) { 88 | h.log.Error("Failed to stream", zap.Error(err)) 89 | return 90 | } 91 | } 92 | 93 | // NewHandler initializes and returns http handler for ranged requests using 94 | // provided StreamerAt as file source and total file size. 95 | func NewHandler(s StreamerAt, size int64) *Handler { 96 | return &Handler{ 97 | log: zap.NewNop(), 98 | size: size, 99 | streamer: s, 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /auth/localization/catalog.go: -------------------------------------------------------------------------------- 1 | package localization 2 | 3 | import ( 4 | "golang.org/x/text/language" 5 | "golang.org/x/text/message" 6 | "golang.org/x/text/message/catalog" 7 | ) 8 | 9 | const ( 10 | // PhoneDialogTitle is key for localized message. 11 | PhoneDialogTitle = "phone_dialog_title" 12 | // PhoneDialogPrompt is key for localized message. 13 | PhoneDialogPrompt = "phone_dialog_prompt" 14 | // PasswordDialogTitle is key for localized message. 15 | PasswordDialogTitle = "password_dialog_title" 16 | // PasswordDialogPrompt is key for localized message. 17 | PasswordDialogPrompt = "password_dialog_prompt" 18 | // TOSDialogTitle is key for localized message. 19 | TOSDialogTitle = "tos_dialog_title" 20 | // TOSDialogPrompt is key for localized message. 21 | TOSDialogPrompt = "tos_dialog_prompt" 22 | // FirstNameDialogTitle is key for localized message. 23 | FirstNameDialogTitle = "first_name_dialog_title" 24 | // FirstNameDialogPrompt is key for localized message. 25 | FirstNameDialogPrompt = "first_name_dialog_prompt" 26 | // SecondNameDialogTitle is key for localized message. 27 | SecondNameDialogTitle = "second_name_dialog_title" 28 | // SecondNameDialogPrompt is key for localized message. 29 | SecondNameDialogPrompt = "second_name_dialog_prompt" 30 | // CodeDialogTitle is key for localized message. 31 | CodeDialogTitle = "code_dialog_title" 32 | // CodeDialogPrompt is key for localized message. 33 | CodeDialogPrompt = "code_dialog_prompt" 34 | // CodeInvalidLength is key for localized message. 35 | CodeInvalidLength = "code_invalid_length" 36 | // CodeDoesNotMatchPattern is key for localized message. 37 | CodeDoesNotMatchPattern = "code_does_not_match_pattern" 38 | ) 39 | 40 | func must(errs ...error) { 41 | for _, err := range errs { 42 | if err != nil { 43 | panic(err) 44 | } 45 | } 46 | } 47 | 48 | // Catalog returns default messages catalog. 49 | func Catalog() *catalog.Builder { 50 | b := catalog.NewBuilder() 51 | eng := language.English 52 | 53 | must( 54 | b.SetString(eng, PhoneDialogTitle, "Your phone"), 55 | b.SetString(eng, PhoneDialogPrompt, "Phone"), 56 | 57 | b.SetString(eng, PasswordDialogTitle, "Your password"), 58 | b.SetString(eng, PasswordDialogPrompt, "Password"), 59 | 60 | b.SetString(eng, TOSDialogTitle, "Telegram requested sign up"), 61 | b.SetString(eng, TOSDialogPrompt, "Accept"), 62 | 63 | b.SetString(eng, FirstNameDialogTitle, "Your first name"), 64 | b.SetString(eng, FirstNameDialogPrompt, "First name"), 65 | 66 | b.SetString(eng, SecondNameDialogTitle, "Your last name"), 67 | b.SetString(eng, SecondNameDialogPrompt, "Last name"), 68 | 69 | b.SetString(eng, CodeDialogTitle, "Verification code"), 70 | b.SetString(eng, CodeDialogPrompt, "Code"), 71 | 72 | b.SetString(eng, CodeInvalidLength, "Code is invalid, length must be %d"), 73 | b.SetString(eng, CodeDoesNotMatchPattern, "Code is invalid, code must match %s"), 74 | ) 75 | return b 76 | } 77 | 78 | // DefaultPrinter returns default localization printer. 79 | func DefaultPrinter() *message.Printer { 80 | return message.NewPrinter( 81 | language.English, 82 | message.Catalog(Catalog()), 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /middleware/floodwait/simple_waiter.go: -------------------------------------------------------------------------------- 1 | package floodwait 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/go-faster/errors" 8 | 9 | "github.com/gotd/td/bin" 10 | "github.com/gotd/td/clock" 11 | "github.com/gotd/td/telegram" 12 | "github.com/gotd/td/tg" 13 | "github.com/gotd/td/tgerr" 14 | ) 15 | 16 | // SimpleWaiter is a tg.Invoker that handles flood wait errors on underlying invoker. 17 | // 18 | // This implementation is more suitable for one-off tasks and programs with low level 19 | // of concurrency and parallelism. 20 | // 21 | // See Waiter for a fully-blown scheduler-based flood wait handler. 22 | type SimpleWaiter struct { 23 | clock clock.Clock 24 | 25 | maxRetries uint 26 | maxWait time.Duration 27 | } 28 | 29 | // NewSimpleWaiter returns a new invoker that waits on the flood wait errors. 30 | func NewSimpleWaiter() *SimpleWaiter { 31 | return &SimpleWaiter{ 32 | clock: clock.System, 33 | } 34 | } 35 | 36 | // clone returns a copy of the SimpleWaiter. 37 | func (w *SimpleWaiter) clone() *SimpleWaiter { 38 | return &SimpleWaiter{ 39 | clock: w.clock, 40 | maxWait: w.maxWait, 41 | maxRetries: w.maxRetries, 42 | } 43 | } 44 | 45 | // WithClock sets clock to use. Default is to use system clock. 46 | func (w *SimpleWaiter) WithClock(c clock.Clock) *SimpleWaiter { 47 | w = w.clone() 48 | w.clock = c 49 | return w 50 | } 51 | 52 | // WithMaxRetries sets max number of retries before giving up. Default is to keep retrying 53 | // on flood wait errors indefinitely. 54 | func (w *SimpleWaiter) WithMaxRetries(m uint) *SimpleWaiter { 55 | w = w.clone() 56 | w.maxRetries = m 57 | return w 58 | } 59 | 60 | // WithMaxWait limits wait time per attempt. SimpleWaiter will return an error if flood wait 61 | // time exceeds that limit. Default is to wait without time limit. 62 | // 63 | // To limit total wait time use a context.Context with timeout or deadline set. 64 | func (w *SimpleWaiter) WithMaxWait(m time.Duration) *SimpleWaiter { 65 | w = w.clone() 66 | w.maxWait = m 67 | return w 68 | } 69 | 70 | // Handle implements telegram.Middleware. 71 | func (w *SimpleWaiter) Handle(next tg.Invoker) telegram.InvokeFunc { 72 | return func(ctx context.Context, input bin.Encoder, output bin.Decoder) error { 73 | var t clock.Timer 74 | 75 | var retries uint 76 | for { 77 | err := next.Invoke(ctx, input, output) 78 | if err == nil { 79 | return nil 80 | } 81 | 82 | d, ok := tgerr.AsFloodWait(err) 83 | if !ok { 84 | return err 85 | } 86 | 87 | retries++ 88 | 89 | if v := w.maxRetries; v != 0 && retries > v { 90 | return errors.Errorf("flood wait retry limit exceeded (%d > %d): %w", retries, v, err) 91 | } 92 | 93 | if d == 0 { 94 | d = time.Second 95 | } 96 | 97 | if v := w.maxWait; v != 0 && d > v { 98 | return errors.Errorf("flood wait argument is too big (%v > %v): %w", d, v, err) 99 | } 100 | 101 | if t == nil { 102 | t = w.clock.Timer(d) 103 | } else { 104 | clock.StopTimer(t) 105 | t.Reset(d) 106 | } 107 | select { 108 | case <-t.C(): 109 | continue 110 | case <-ctx.Done(): 111 | clock.StopTimer(t) 112 | return ctx.Err() 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /storage/collector_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/gotd/td/telegram/query/channels/participants" 10 | "github.com/gotd/td/telegram/query/dialogs" 11 | "github.com/gotd/td/tg" 12 | ) 13 | 14 | func testUser() *tg.User { 15 | return &tg.User{ 16 | ID: 10, 17 | AccessHash: 10, 18 | FirstName: "Жак", 19 | LastName: "Фреско", 20 | Username: "zagadka1337", 21 | } 22 | } 23 | 24 | type dialogQuery struct { 25 | result *tg.MessagesDialogs 26 | } 27 | 28 | func (d dialogQuery) Query(ctx context.Context, req dialogs.Request) (tg.MessagesDialogsClass, error) { 29 | return d.result, nil 30 | } 31 | 32 | func TestPeerCollector_Dialogs(t *testing.T) { 33 | a := require.New(t) 34 | mem := newMemStorage() 35 | collector := CollectPeers(mem) 36 | ctx := context.Background() 37 | 38 | user := testUser() 39 | iter := dialogs.NewIterator(dialogQuery{ 40 | result: &tg.MessagesDialogs{ 41 | Dialogs: []tg.DialogClass{ 42 | &tg.Dialog{ 43 | Pinned: false, 44 | UnreadMark: false, 45 | Peer: &tg.PeerUser{UserID: 10}, 46 | TopMessage: 1, 47 | }, 48 | }, 49 | Messages: []tg.MessageClass{ 50 | &tg.Message{ 51 | ID: 1, 52 | PeerID: &tg.PeerUser{UserID: 10}, 53 | Message: "бебебе с бабаба", 54 | }, 55 | }, 56 | Users: []tg.UserClass{user}, 57 | }, 58 | }, 1) 59 | a.NoError(collector.Dialogs(ctx, iter)) 60 | 61 | p, err := mem.Resolve(ctx, user.Username) 62 | a.NoError(err) 63 | a.NotNil(p.User) 64 | a.Equal(user.FirstName, p.User.FirstName) 65 | } 66 | 67 | type participantsQuery struct { 68 | result *tg.ChannelsChannelParticipants 69 | } 70 | 71 | func (p *participantsQuery) Query(ctx context.Context, req participants.Request) (tg.ChannelsChannelParticipantsClass, error) { 72 | p.result.Participants = p.result.Participants[req.Offset:] 73 | return p.result, nil 74 | } 75 | 76 | func TestPeerCollector_Participants(t *testing.T) { 77 | a := require.New(t) 78 | mem := newMemStorage() 79 | collector := CollectPeers(mem) 80 | ctx := context.Background() 81 | 82 | user := testUser() 83 | iter := participants.NewIterator(&participantsQuery{ 84 | result: &tg.ChannelsChannelParticipants{ 85 | Count: 1, 86 | Participants: []tg.ChannelParticipantClass{ 87 | &tg.ChannelParticipantCreator{ 88 | UserID: user.ID, 89 | AdminRights: tg.ChatAdminRights{}, 90 | Rank: "фреска", 91 | }, 92 | }, 93 | Users: []tg.UserClass{user}, 94 | }, 95 | }, 1) 96 | a.NoError(collector.Participants(ctx, iter)) 97 | 98 | p, err := mem.Resolve(ctx, "zagadka1337") 99 | a.NoError(err) 100 | a.NotNil(p.User) 101 | a.Equal("Жак", p.User.FirstName) 102 | } 103 | 104 | func TestPeerCollector_Contacts(t *testing.T) { 105 | a := require.New(t) 106 | mem := newMemStorage() 107 | collector := CollectPeers(mem) 108 | ctx := context.Background() 109 | 110 | user := testUser() 111 | a.NoError(collector.Contacts(ctx, &tg.ContactsContacts{ 112 | Users: []tg.UserClass{user}, 113 | })) 114 | 115 | p, err := mem.Resolve(ctx, "zagadka1337") 116 | a.NoError(err) 117 | a.NotNil(p.User) 118 | a.Equal("Жак", p.User.FirstName) 119 | } 120 | -------------------------------------------------------------------------------- /http_range/range.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package http_range implements http range parsing. 6 | package http_range 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "net/textproto" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | // Range specifies the byte range to be sent to the client. 17 | type Range struct { 18 | Start int64 19 | Length int64 20 | } 21 | 22 | // ContentRange returns Content-Range header value. 23 | func (r Range) ContentRange(size int64) string { 24 | return fmt.Sprintf("bytes %d-%d/%d", r.Start, r.Start+r.Length-1, size) 25 | } 26 | 27 | var ( 28 | // ErrNoOverlap is returned by ParseRange if first-byte-pos of 29 | // all of the byte-range-spec values is greater than the content size. 30 | ErrNoOverlap = errors.New("invalid range: failed to overlap") 31 | 32 | // ErrInvalid is returned by ParseRange on invalid input. 33 | ErrInvalid = errors.New("invalid range") 34 | ) 35 | 36 | // ParseRange parses a Range header string as per RFC 7233. 37 | // ErrNoOverlap is returned if none of the ranges overlap. 38 | // ErrInvalid is returned if s is invalid range. 39 | func ParseRange(s string, size int64) ([]Range, error) { // nolint:gocognit 40 | if s == "" { 41 | return nil, nil // header not present 42 | } 43 | const b = "bytes=" 44 | if !strings.HasPrefix(s, b) { 45 | return nil, ErrInvalid 46 | } 47 | var ranges []Range 48 | noOverlap := false 49 | for _, ra := range strings.Split(s[len(b):], ",") { 50 | ra = textproto.TrimString(ra) 51 | if ra == "" { 52 | continue 53 | } 54 | i := strings.Index(ra, "-") 55 | if i < 0 { 56 | return nil, ErrInvalid 57 | } 58 | start, end := textproto.TrimString(ra[:i]), textproto.TrimString(ra[i+1:]) 59 | var r Range 60 | if start == "" { 61 | // If no start is specified, end specifies the 62 | // range start relative to the end of the file, 63 | // and we are dealing with 64 | // which has to be a non-negative integer as per 65 | // RFC 7233 Section 2.1 "Byte-Ranges". 66 | if end == "" || end[0] == '-' { 67 | return nil, ErrInvalid 68 | } 69 | i, err := strconv.ParseInt(end, 10, 64) 70 | if i < 0 || err != nil { 71 | return nil, ErrInvalid 72 | } 73 | if i > size { 74 | i = size 75 | } 76 | r.Start = size - i 77 | r.Length = size - r.Start 78 | } else { 79 | i, err := strconv.ParseInt(start, 10, 64) 80 | if err != nil || i < 0 { 81 | return nil, ErrInvalid 82 | } 83 | if i >= size { 84 | // If the range begins after the size of the content, 85 | // then it does not overlap. 86 | noOverlap = true 87 | continue 88 | } 89 | r.Start = i 90 | if end == "" { 91 | // If no end is specified, range extends to end of the file. 92 | r.Length = size - r.Start 93 | } else { 94 | i, err := strconv.ParseInt(end, 10, 64) 95 | if err != nil || r.Start > i { 96 | return nil, ErrInvalid 97 | } 98 | if i >= size { 99 | i = size - 1 100 | } 101 | r.Length = i - r.Start + 1 102 | } 103 | } 104 | ranges = append(ranges, r) 105 | } 106 | if noOverlap && len(ranges) == 0 { 107 | // The specified ranges did not overlap with the content. 108 | return nil, ErrNoOverlap 109 | } 110 | return ranges, nil 111 | } 112 | -------------------------------------------------------------------------------- /storage/resolver_cache_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/go-faster/errors" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/gotd/td/tg" 11 | ) 12 | 13 | type memStorage struct { 14 | peers map[PeerKey]Peer 15 | keys map[string]PeerKey 16 | } 17 | 18 | func newMemStorage() memStorage { 19 | return memStorage{ 20 | peers: map[PeerKey]Peer{}, 21 | keys: map[string]PeerKey{}, 22 | } 23 | } 24 | 25 | func (m memStorage) Iterate(ctx context.Context) (PeerIterator, error) { 26 | return nil, errors.New("unimplemented") 27 | } 28 | 29 | func (m memStorage) add(keys []string, p Peer) { 30 | id := KeyFromPeer(p) 31 | m.peers[id] = p 32 | for _, key := range keys { 33 | m.keys[key] = id 34 | } 35 | } 36 | 37 | func (m memStorage) Add(ctx context.Context, p Peer) error { 38 | m.add(p.Keys(), p) 39 | return nil 40 | } 41 | 42 | func (m memStorage) Find(ctx context.Context, key PeerKey) (Peer, error) { 43 | v, ok := m.peers[key] 44 | if !ok { 45 | return Peer{}, ErrPeerNotFound 46 | } 47 | return v, nil 48 | } 49 | 50 | func (m memStorage) Assign(ctx context.Context, key string, p Peer) error { 51 | m.add(append(p.Keys(), key), p) 52 | return nil 53 | } 54 | 55 | func (m memStorage) Resolve(ctx context.Context, key string) (Peer, error) { 56 | id, ok := m.keys[key] 57 | if !ok { 58 | return Peer{}, ErrPeerNotFound 59 | } 60 | 61 | v, ok := m.peers[id] 62 | if !ok { 63 | return Peer{}, ErrPeerNotFound 64 | } 65 | return v, nil 66 | } 67 | 68 | type resolverFunc func(ctx context.Context, domain string) (tg.InputPeerClass, error) 69 | 70 | func (r resolverFunc) ResolveDomain(ctx context.Context, domain string) (tg.InputPeerClass, error) { 71 | return r(ctx, domain) 72 | } 73 | 74 | func (r resolverFunc) ResolvePhone(ctx context.Context, phone string) (tg.InputPeerClass, error) { 75 | return r(ctx, phone) 76 | } 77 | 78 | func TestResolverCache(t *testing.T) { 79 | t.Run("Domain", func(t *testing.T) { 80 | a := require.New(t) 81 | ctx := context.Background() 82 | expected := &tg.InputPeerUser{ 83 | UserID: 10, 84 | AccessHash: 10, 85 | } 86 | expectedKey := "abc" 87 | counter := 0 88 | 89 | r := func(ctx context.Context, k string) (tg.InputPeerClass, error) { 90 | a.Equal(expectedKey, k) 91 | a.Zero(counter) 92 | counter++ 93 | return expected, nil 94 | } 95 | c := NewResolverCache(resolverFunc(r), newMemStorage()) 96 | 97 | result, err := c.ResolveDomain(ctx, "abc") 98 | a.NoError(err) 99 | a.Equal(expected, result) 100 | 101 | result, err = c.ResolveDomain(ctx, "abc") 102 | a.NoError(err) 103 | a.Equal(expected, result) 104 | }) 105 | 106 | t.Run("Phone", func(t *testing.T) { 107 | a := require.New(t) 108 | ctx := context.Background() 109 | expected := &tg.InputPeerUser{ 110 | UserID: 10, 111 | AccessHash: 10, 112 | } 113 | expectedKey := "abc" 114 | counter := 0 115 | 116 | r := func(ctx context.Context, k string) (tg.InputPeerClass, error) { 117 | a.Equal(expectedKey, k) 118 | a.Zero(counter) 119 | counter++ 120 | return expected, nil 121 | } 122 | c := NewResolverCache(resolverFunc(r), newMemStorage()) 123 | 124 | result, err := c.ResolvePhone(ctx, "abc") 125 | a.NoError(err) 126 | a.Equal(expected, result) 127 | 128 | result, err = c.ResolvePhone(ctx, "abc") 129 | a.NoError(err) 130 | a.Equal(expected, result) 131 | }) 132 | } 133 | -------------------------------------------------------------------------------- /oteltg/middleware.go: -------------------------------------------------------------------------------- 1 | // Package oteltg provides OpenTelemetry instrumentation for gotd. 2 | package oteltg 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "strconv" 8 | "time" 9 | 10 | "go.opentelemetry.io/otel/attribute" 11 | "go.opentelemetry.io/otel/codes" 12 | "go.opentelemetry.io/otel/metric" 13 | "go.opentelemetry.io/otel/trace" 14 | 15 | "github.com/gotd/td/bin" 16 | "github.com/gotd/td/telegram" 17 | "github.com/gotd/td/tg" 18 | "github.com/gotd/td/tgerr" 19 | ) 20 | 21 | // Middleware is prometheus metrics middleware for Telegram. 22 | type Middleware struct { 23 | count metric.Int64Counter 24 | failures metric.Int64Counter 25 | duration metric.Float64Histogram 26 | tracer trace.Tracer 27 | } 28 | 29 | // Handle implements telegram.Middleware. 30 | func (m Middleware) Handle(next tg.Invoker) telegram.InvokeFunc { 31 | return func(ctx context.Context, input bin.Encoder, output bin.Decoder) error { 32 | // Prepare. 33 | attrs := m.attributes(input) 34 | 35 | spanName := "tg.rpc" 36 | for _, attr := range attrs { 37 | if attr.Key == "tg.method" { 38 | spanName = fmt.Sprintf("%s: %s", spanName, attr.Value.AsString()) 39 | } 40 | } 41 | 42 | ctx, span := m.tracer.Start(ctx, spanName, trace.WithAttributes(attrs...)) 43 | defer span.End() 44 | m.count.Add(ctx, 1, metric.WithAttributes(attrs...)) 45 | start := time.Now() 46 | 47 | // Call actual method. 48 | err := next.Invoke(ctx, input, output) 49 | 50 | // Observe. 51 | m.duration.Record(ctx, time.Since(start).Seconds(), metric.WithAttributes(attrs...)) 52 | if err != nil { 53 | var errAttrs []attribute.KeyValue 54 | if rpcErr, ok := tgerr.As(err); ok { 55 | span.SetStatus(codes.Error, "RPC error") 56 | errAttrs = append(errAttrs, 57 | attribute.String("tg.rpc.err", rpcErr.Type), 58 | attribute.String("tg.rpc.code", strconv.Itoa(rpcErr.Code)), 59 | ) 60 | } else { 61 | span.SetStatus(codes.Error, "Internal error") 62 | errAttrs = append(errAttrs, 63 | attribute.String("tg.rpc.err", "CLIENT"), 64 | ) 65 | } 66 | span.RecordError(err, trace.WithAttributes(errAttrs...)) 67 | attrs = append(attrs, errAttrs...) 68 | m.failures.Add(ctx, 1, metric.WithAttributes(attrs...)) 69 | } else { 70 | span.SetStatus(codes.Ok, "") 71 | } 72 | 73 | return err 74 | } 75 | } 76 | 77 | // object is a abstraction for Telegram API object with TypeName. 78 | type object interface { 79 | TypeName() string 80 | } 81 | 82 | func (m Middleware) attributes(input bin.Encoder) []attribute.KeyValue { 83 | obj, ok := input.(object) 84 | if !ok { 85 | return []attribute.KeyValue{} 86 | } 87 | return []attribute.KeyValue{ 88 | attribute.String("tg.method", obj.TypeName()), 89 | } 90 | } 91 | 92 | // New initializes and returns new prometheus middleware. 93 | func New(meterProvider metric.MeterProvider, tracerProvider trace.TracerProvider) (*Middleware, error) { 94 | const name = "github.com/gotd/contrib/oteltg" 95 | meter := meterProvider.Meter(name) 96 | m := &Middleware{ 97 | tracer: tracerProvider.Tracer(name), 98 | } 99 | var err error 100 | if m.count, err = meter.Int64Counter("tg.rpc.count"); err != nil { 101 | return nil, err 102 | } 103 | if m.failures, err = meter.Int64Counter("tg.rpc.failures"); err != nil { 104 | return nil, err 105 | } 106 | if m.duration, err = meter.Float64Histogram("tg.rpc.duration"); err != nil { 107 | return nil, err 108 | } 109 | return m, nil 110 | } 111 | -------------------------------------------------------------------------------- /internal/tests/storage.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/gotd/td/session" 11 | "github.com/gotd/td/tg" 12 | 13 | "github.com/gotd/contrib/auth" 14 | "github.com/gotd/contrib/storage" 15 | ) 16 | 17 | // Credentials is a KV credential storage abstraction. 18 | type Credentials interface { 19 | auth.Credentials 20 | SavePhone(ctx context.Context, phone string) error 21 | SavePassword(ctx context.Context, password string) error 22 | } 23 | 24 | // TestSessionStorage runs different tests for given session storage implementation. 25 | func TestSessionStorage(t *testing.T, s session.Storage) { 26 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 27 | defer cancel() 28 | 29 | t.Run("Session", func(t *testing.T) { 30 | a := require.New(t) 31 | 32 | data := []byte("mytoken") 33 | _, err := s.LoadSession(ctx) 34 | a.Error(err, "no session expected") 35 | a.NoError(s.StoreSession(ctx, data)) 36 | 37 | vaultData, err := s.LoadSession(ctx) 38 | a.NoError(err) 39 | a.Equal(data, vaultData) 40 | }) 41 | } 42 | 43 | // TestCredentials runs different tests for given credentials storage implementation. 44 | func TestCredentials(t *testing.T, cred Credentials) { 45 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 46 | defer cancel() 47 | 48 | t.Run("Credentials", func(t *testing.T) { 49 | a := require.New(t) 50 | 51 | phone, password := "phone", "password" 52 | a.NoError(cred.SavePhone(ctx, phone)) 53 | a.NoError(cred.SavePassword(ctx, password)) 54 | 55 | gotPhone, err := cred.Phone(ctx) 56 | a.NoError(err) 57 | a.Equal(phone, gotPhone) 58 | 59 | gotPassword, err := cred.Password(ctx) 60 | a.NoError(err) 61 | a.Equal(password, gotPassword) 62 | }) 63 | } 64 | 65 | // TestPeerStorage runs different tests for given peer storage implementation. 66 | func TestPeerStorage(t *testing.T, st storage.PeerStorage) { 67 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 68 | defer cancel() 69 | 70 | t.Run("PeerStorage", func(t *testing.T) { 71 | a := require.New(t) 72 | 73 | _, err := st.Resolve(ctx, "abc") 74 | a.ErrorIs(err, storage.ErrPeerNotFound) 75 | 76 | var p storage.Peer 77 | a.NoError(p.FromInputPeer(&tg.InputPeerUser{ 78 | UserID: 10, 79 | AccessHash: 10, 80 | })) 81 | key := storage.KeyFromPeer(p) 82 | 83 | _, err = st.Find(ctx, key) 84 | a.ErrorIs(err, storage.ErrPeerNotFound) 85 | 86 | a.NoError(st.Add(ctx, p)) 87 | _, err = st.Find(ctx, key) 88 | a.NoError(err) 89 | 90 | a.NoError(st.Assign(ctx, "abc", p)) 91 | _, err = st.Resolve(ctx, "abc") 92 | a.NoError(err) 93 | 94 | for i := range [5]struct{}{} { 95 | a.NoError(p.FromInputPeer(&tg.InputPeerUser{ 96 | UserID: int64(i) + 11, 97 | AccessHash: int64(i) + 11, 98 | })) 99 | a.NoError(st.Add(ctx, p)) 100 | } 101 | 102 | iter, err := st.Iterate(ctx) 103 | a.NoError(err) 104 | defer func() { 105 | a.NoError(iter.Close()) 106 | }() 107 | 108 | var peers []storage.Peer 109 | for iter.Next(ctx) { 110 | peers = append(peers, iter.Value()) 111 | } 112 | if err := iter.Err(); err != nil { 113 | a.NoError(err) 114 | } 115 | 116 | a.GreaterOrEqual(len(peers), 6) 117 | var found bool 118 | for _, vp := range peers { 119 | if vp.Key != p.Key { 120 | continue 121 | } 122 | found = true 123 | break 124 | } 125 | a.True(found, "should contain") 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /partio/streamer_test.go: -------------------------------------------------------------------------------- 1 | package partio 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestPadding(t *testing.T) { 15 | for _, tt := range []struct { 16 | padding, offset, result int64 17 | }{ 18 | {}, 19 | { 20 | padding: 8, 21 | offset: 2, 22 | result: 0, 23 | }, 24 | { 25 | padding: 8, 26 | offset: 9, 27 | result: 8, 28 | }, 29 | { 30 | padding: 8, 31 | offset: 8, 32 | result: 8, 33 | }, 34 | { 35 | padding: 1024, 36 | offset: 413, 37 | result: 0, 38 | }, 39 | { 40 | offset: 514, 41 | result: 514, 42 | }, 43 | } { 44 | t.Run(fmt.Sprintf("%d_%d", tt.padding, tt.offset), func(t *testing.T) { 45 | require.Equal(t, tt.result, nearestOffset(tt.padding, tt.offset)) 46 | }) 47 | } 48 | } 49 | 50 | type BytesReader struct { 51 | Data []byte 52 | Align int64 53 | } 54 | 55 | func (r BytesReader) Chunk(ctx context.Context, offset int64, b []byte) (int64, error) { 56 | if offset%r.Align != 0 { 57 | return 0, errors.New("unaligned") 58 | } 59 | if int64(len(b)) != r.Align { 60 | return 0, errors.New("invalid chunk size") 61 | } 62 | 63 | if offset > int64(len(r.Data)) { 64 | return 0, io.EOF 65 | } 66 | buf := r.Data[offset:] 67 | n := int64(copy(b, buf)) 68 | if n != r.Align { 69 | return n, io.EOF 70 | } 71 | 72 | return n, nil 73 | } 74 | 75 | type StreamReader struct { 76 | Align int64 77 | Total int64 78 | } 79 | 80 | func (r StreamReader) Chunk(ctx context.Context, offset int64, b []byte) (int64, error) { 81 | if offset%r.Align != 0 { 82 | return 0, errors.New("unaligned") 83 | } 84 | if int64(len(b)) != r.Align { 85 | return 0, errors.New("invalid chunk size") 86 | } 87 | if offset > r.Total { 88 | return 0, io.EOF 89 | } 90 | 91 | n := r.Align 92 | if (offset + n) > r.Total { 93 | // Last chunk. 94 | n = r.Total - offset 95 | } 96 | for i := range b { 97 | b[i] = byte(i) 98 | } 99 | if n != r.Align { 100 | return n, io.EOF 101 | } 102 | return n, nil 103 | } 104 | 105 | func TestReaderFrom_Stream(t *testing.T) { 106 | ctx := context.Background() 107 | t.Run("Simple", func(t *testing.T) { 108 | const chunkSize = 3 109 | s := NewStreamer(&BytesReader{ 110 | Align: chunkSize, 111 | Data: []byte{1, 2, 3, 4, 5, 6, 7, 8}, 112 | }, chunkSize) 113 | t.Run("Equal", func(t *testing.T) { 114 | out := new(bytes.Buffer) 115 | require.NoError(t, s.StreamAt(ctx, 2, out)) 116 | require.Equal(t, []byte{3, 4, 5, 6, 7, 8}, out.Bytes()) 117 | }) 118 | t.Run("Discard", func(t *testing.T) { 119 | require.NoError(t, s.Stream(ctx, io.Discard)) 120 | }) 121 | }) 122 | t.Run("Stream", func(t *testing.T) { 123 | const ( 124 | chunkSize = 1024 125 | total = chunkSize*100 + 56 126 | ) 127 | s := NewStreamer(&StreamReader{ 128 | Align: chunkSize, 129 | Total: total, 130 | }, chunkSize) 131 | t.Run("Equal", func(t *testing.T) { 132 | buf := new(bytes.Buffer) 133 | require.NoError(t, s.StreamAt(ctx, total-chunkSize, buf)) 134 | require.Equal(t, byte(56), buf.Bytes()[0]) 135 | require.Equal(t, 1024, buf.Len()) 136 | }) 137 | t.Run("Discard", func(t *testing.T) { 138 | require.NoError(t, s.Stream(ctx, io.Discard)) 139 | }) 140 | }) 141 | } 142 | 143 | func BenchmarkReaderFrom_StreamAt(b *testing.B) { 144 | const ( 145 | chunkSize = 1024 146 | total = chunkSize*100 + 56 147 | offset = chunkSize/2 + 10 148 | ) 149 | s := NewStreamer(&StreamReader{ 150 | Align: chunkSize, 151 | Total: total, 152 | }, chunkSize) 153 | 154 | b.ReportAllocs() 155 | b.SetBytes(total - offset) 156 | 157 | for i := 0; i < b.N; i++ { 158 | if err := s.StreamAt(context.Background(), offset, io.Discard); err != nil { 159 | b.Fatal() 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /auth/terminal/code.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | "strings" 8 | 9 | "github.com/go-faster/errors" 10 | "golang.org/x/term" 11 | "golang.org/x/text/message" 12 | 13 | tgauth "github.com/gotd/td/telegram/auth" 14 | 15 | "github.com/gotd/td/tg" 16 | 17 | "github.com/gotd/contrib/auth/localization" 18 | ) 19 | 20 | var _ tgauth.UserAuthenticator = (*Terminal)(nil) 21 | 22 | // Terminal implements UserAuthenticator. 23 | type Terminal struct { 24 | *term.Terminal 25 | printer *message.Printer 26 | } 27 | 28 | // New creates new Terminal. 29 | func New(in io.Reader, out io.Writer) *Terminal { 30 | rw := struct { 31 | io.Reader 32 | io.Writer 33 | }{ 34 | Reader: in, 35 | Writer: out, 36 | } 37 | return &Terminal{ 38 | Terminal: term.NewTerminal(rw, ""), 39 | printer: localization.DefaultPrinter(), 40 | } 41 | } 42 | 43 | // OS creates new Terminal using os.Stdout and os.Stdin. 44 | func OS() *Terminal { 45 | return New(os.Stdin, os.Stdout) 46 | } 47 | 48 | // WithPrinter sets localization printer. 49 | func (t *Terminal) WithPrinter(printer *message.Printer) *Terminal { 50 | t.printer = printer 51 | return t 52 | } 53 | 54 | func (t *Terminal) read(prompt string) (string, error) { 55 | t.SetPrompt(prompt) 56 | defer t.SetPrompt("") 57 | return t.ReadLine() 58 | } 59 | 60 | // Phone asks phone using terminal. 61 | func (t *Terminal) Phone(ctx context.Context) (string, error) { 62 | return t.read(t.printer.Sprintf(localization.PhoneDialogPrompt) + ":") 63 | } 64 | 65 | // Password asks password using terminal. 66 | func (t *Terminal) Password(ctx context.Context) (string, error) { 67 | return t.read(t.printer.Sprintf(localization.PasswordDialogPrompt) + ":") 68 | } 69 | 70 | // Code asks code using terminal. 71 | func (t *Terminal) Code(ctx context.Context, sentCode *tg.AuthSentCode) (string, error) { 72 | prompt := t.printer.Sprintf(localization.CodeDialogPrompt) 73 | for { 74 | code, err := t.read(prompt + ":") 75 | if err != nil { 76 | return "", err 77 | } 78 | code = strings.TrimSpace(code) 79 | 80 | type notFlashing interface { 81 | GetLength() int 82 | } 83 | 84 | switch v := sentCode.Type.(type) { 85 | case notFlashing: 86 | length := v.GetLength() 87 | if len(code) != length { 88 | _, err := io.WriteString(t.Terminal, t.printer.Sprintf(localization.CodeInvalidLength, length)+"\n") 89 | if err != nil { 90 | return "", errors.Errorf("write error message: %w", err) 91 | } 92 | continue 93 | } 94 | 95 | return code, nil 96 | // TODO: add tg.AuthSentCodeTypeFlashCall support 97 | default: 98 | return code, nil 99 | } 100 | } 101 | } 102 | 103 | // SignUp asks user info for sign up. 104 | func (t *Terminal) SignUp(ctx context.Context) (u tgauth.UserInfo, err error) { 105 | u.FirstName, err = t.read(t.printer.Sprintf(localization.FirstNameDialogPrompt) + ":") 106 | if err != nil { 107 | return u, errors.Errorf("read first name: %w", err) 108 | } 109 | 110 | u.LastName, err = t.read(t.printer.Sprintf(localization.SecondNameDialogPrompt) + ":") 111 | if err != nil { 112 | return u, errors.Errorf("read first name: %w", err) 113 | } 114 | 115 | return u, nil 116 | } 117 | 118 | // AcceptTermsOfService write terms of service received from Telegram and 119 | // asks to accept it. 120 | func (t *Terminal) AcceptTermsOfService(ctx context.Context, tos tg.HelpTermsOfService) error { 121 | _, err := io.WriteString(t.Terminal, t.printer.Sprintf(localization.TOSDialogTitle)+"\n\n"+tos.Text) 122 | if err != nil { 123 | return errors.Errorf("write terms of service: %w", err) 124 | } 125 | 126 | t.SetPrompt(t.printer.Sprintf(localization.TOSDialogPrompt) + "(Y/N):") 127 | defer t.SetPrompt("") 128 | 129 | loop: 130 | y, err := t.ReadLine() 131 | if err != nil { 132 | return errors.Errorf("read answer: %w", err) 133 | } 134 | switch strings.ToLower(y) { 135 | case "y", "yes": 136 | return nil 137 | case "n", "no": 138 | return errors.New("user answer is no") 139 | default: 140 | goto loop 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /partio/streamer.go: -------------------------------------------------------------------------------- 1 | // Package partio implements chunk-based input/output where aligning is 2 | // required. 3 | package partio 4 | 5 | import ( 6 | "context" 7 | "io" 8 | "time" 9 | 10 | "github.com/go-faster/errors" 11 | ) 12 | 13 | // ChunkSource downloads chunks. 14 | type ChunkSource interface { 15 | Chunk(ctx context.Context, offset int64, b []byte) (int64, error) 16 | } 17 | 18 | // Streamer provides a pseudo-stream. 19 | type Streamer struct { 20 | align int64 // required chunk size 21 | source ChunkSource // source of chunks 22 | } 23 | 24 | // nearestOffset returns nearest offset that will conform to aligning 25 | // requirements. 26 | func nearestOffset(align, offset int64) int64 { 27 | if align == 0 { 28 | return offset 29 | } 30 | if offset == 0 { 31 | return 0 32 | } 33 | return offset - (offset % align) 34 | } 35 | 36 | func (s Streamer) safeRead(ctx context.Context, offset int64, data []byte) (int64, error) { 37 | n, err := s.source.Chunk(ctx, offset, data) 38 | if err != nil { 39 | return n, err 40 | } 41 | if n < 0 || n > int64(len(data)) { 42 | return n, errors.Errorf("invalid chunk: %d", n) 43 | } 44 | 45 | return n, nil 46 | } 47 | 48 | // errInvalidWrite means that a write returned an impossible count. 49 | var errInvalidWrite = errors.New("invalid write result") 50 | 51 | func checkDone(ctx context.Context) error { 52 | select { 53 | case <-ctx.Done(): 54 | return ctx.Err() 55 | default: 56 | return nil 57 | } 58 | } 59 | 60 | // TimedChunkSource wraps ChunkSource with Timeout for each chunk 61 | type TimedChunkSource struct { 62 | ChunkSource 63 | Timeout time.Duration 64 | } 65 | 66 | // Chunk implements ChunkSource with Timeout for each chunk. 67 | func (s TimedChunkSource) Chunk(ctx context.Context, offset int64, b []byte) (int64, error) { 68 | ctx, cancel := context.WithTimeout(ctx, s.Timeout) 69 | defer cancel() 70 | 71 | return s.ChunkSource.Chunk(ctx, offset, b) 72 | } 73 | 74 | func (s Streamer) writeFull(ctx context.Context, buf []byte, dst io.Writer) (written int64, err error) { 75 | nr := len(buf) 76 | 77 | for { 78 | if err = checkDone(ctx); err != nil { 79 | break 80 | } 81 | if written == int64(nr) { 82 | break 83 | } 84 | // Same logic as in io.Copy. 85 | nw, ew := dst.Write(buf[written:nr]) 86 | if nw < 0 || nr < nw { 87 | nw = 0 88 | if ew == nil { 89 | ew = errInvalidWrite 90 | } 91 | } 92 | written += int64(nw) 93 | if ew != nil { 94 | err = ew 95 | break 96 | } 97 | if nr != nw { 98 | err = io.ErrShortWrite 99 | break 100 | } 101 | } 102 | 103 | return written, err 104 | } 105 | 106 | // Stream is shorthand for StreamAt that streams from the beginning. 107 | func (s Streamer) Stream(ctx context.Context, w io.Writer) error { 108 | return s.StreamAt(ctx, 0, w) 109 | } 110 | 111 | // StreamAt streams from reader to "w" with "skip" offset. 112 | func (s Streamer) StreamAt(ctx context.Context, skip int64, w io.Writer) error { 113 | var ( 114 | buf = make([]byte, s.align) 115 | offset = nearestOffset(s.align, skip) 116 | bufSkip = skip - offset 117 | ) 118 | for { 119 | if err := checkDone(ctx); err != nil { 120 | return err 121 | } 122 | nr, er := s.safeRead(ctx, offset, buf) 123 | if er != nil && er != io.EOF { 124 | // Reading side done. 125 | return er 126 | } 127 | if nr > 0 { 128 | if _, err := s.writeFull(ctx, buf[bufSkip:nr], w); err != nil { 129 | // Writing side done. 130 | return err 131 | } 132 | } 133 | if er == io.EOF { 134 | // Reading side exhausted. 135 | return nil 136 | } 137 | 138 | // Continue. 139 | offset += s.align // next chunk 140 | bufSkip = 0 // only skip at first chunk 141 | } 142 | } 143 | 144 | // NewStreamer initializes and returns new *Streamer using provided chunk 145 | // source and chunk size. 146 | func NewStreamer(r ChunkSource, chunkSize int64) *Streamer { 147 | if chunkSize <= 0 { 148 | panic("invalid chunk size") 149 | } 150 | return &Streamer{ 151 | align: chunkSize, 152 | source: r, 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /auth/dialog/dialog.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/gen2brain/dlgs" 8 | "github.com/go-faster/errors" 9 | "golang.org/x/text/message" 10 | 11 | tgauth "github.com/gotd/td/telegram/auth" 12 | "github.com/gotd/td/tg" 13 | 14 | "github.com/gotd/contrib/auth/localization" 15 | ) 16 | 17 | var _ tgauth.UserAuthenticator = Dialog{} 18 | 19 | // Dialog is authenticator implementation using GUI dialogs. 20 | type Dialog struct { 21 | printer *message.Printer 22 | } 23 | 24 | // NewDialog creates new Dialog. 25 | func NewDialog() Dialog { 26 | return Dialog{ 27 | printer: localization.DefaultPrinter(), 28 | } 29 | } 30 | 31 | // WithPrinter sets localization printer. 32 | func (d Dialog) WithPrinter(printer *message.Printer) Dialog { 33 | d.printer = printer 34 | return d 35 | } 36 | 37 | var errDialogClosed = errors.New("dialog closed") 38 | 39 | // Phone implements telegram.UserAuthenticator. 40 | func (d Dialog) Phone(ctx context.Context) (string, error) { 41 | r, ok, err := dlgs.Entry( 42 | d.printer.Sprintf(localization.PhoneDialogTitle), 43 | d.printer.Sprintf(localization.PhoneDialogPrompt), 44 | "", 45 | ) 46 | if err != nil { 47 | return "", errors.Errorf("show dialog: %w", err) 48 | } 49 | if !ok { 50 | return "", errDialogClosed 51 | } 52 | 53 | return r, nil 54 | } 55 | 56 | // Password implements telegram.UserAuthenticator. 57 | func (d Dialog) Password(ctx context.Context) (string, error) { 58 | r, ok, err := dlgs.Password( 59 | d.printer.Sprintf(localization.PasswordDialogTitle), 60 | d.printer.Sprintf(localization.PasswordDialogPrompt), 61 | ) 62 | if err != nil { 63 | return "", errors.Errorf("show dialog: %w", err) 64 | } 65 | if !ok { 66 | return "", errDialogClosed 67 | } 68 | 69 | return r, nil 70 | } 71 | 72 | // AcceptTermsOfService implements telegram.UserAuthenticator. 73 | func (d Dialog) AcceptTermsOfService(ctx context.Context, tos tg.HelpTermsOfService) error { 74 | ok, err := dlgs.Question( 75 | d.printer.Sprintf(localization.TOSDialogTitle), 76 | tos.Text, 77 | false, 78 | ) 79 | if err != nil { 80 | return errors.Errorf("show dialog: %w", err) 81 | } 82 | if !ok { 83 | return errDialogClosed 84 | } 85 | 86 | return nil 87 | } 88 | 89 | // SignUp implements telegram.UserAuthenticator. 90 | func (d Dialog) SignUp(ctx context.Context) (tgauth.UserInfo, error) { 91 | firstName, ok, err := dlgs.Entry( 92 | d.printer.Sprintf(localization.FirstNameDialogTitle), 93 | d.printer.Sprintf(localization.FirstNameDialogPrompt), 94 | "", 95 | ) 96 | if err != nil { 97 | return tgauth.UserInfo{}, errors.Errorf("show dialog: %w", err) 98 | } 99 | if !ok { 100 | return tgauth.UserInfo{}, errDialogClosed 101 | } 102 | 103 | secondName, ok, err := dlgs.Entry( 104 | d.printer.Sprintf(localization.SecondNameDialogTitle), 105 | d.printer.Sprintf(localization.SecondNameDialogPrompt), 106 | "", 107 | ) 108 | if err != nil { 109 | return tgauth.UserInfo{}, errors.Errorf("show dialog: %w", err) 110 | } 111 | if !ok { 112 | return tgauth.UserInfo{}, errDialogClosed 113 | } 114 | 115 | return tgauth.UserInfo{ 116 | FirstName: firstName, 117 | LastName: secondName, 118 | }, nil 119 | } 120 | 121 | // Code implements telegram.UserAuthenticator. 122 | func (d Dialog) Code(ctx context.Context, sentCode *tg.AuthSentCode) (string, error) { 123 | title := d.printer.Sprintf(localization.CodeDialogTitle) 124 | prompt := d.printer.Sprintf(localization.CodeDialogPrompt) 125 | for { 126 | code, ok, err := dlgs.Entry(title, prompt, "") 127 | if err != nil { 128 | return "", errors.Errorf("show dialog: %w", err) 129 | } 130 | if !ok { 131 | return "", errDialogClosed 132 | } 133 | 134 | code = strings.TrimSpace(code) 135 | 136 | type notFlashing interface { 137 | GetLength() int 138 | } 139 | 140 | switch v := sentCode.Type.(type) { 141 | case notFlashing: 142 | length := v.GetLength() 143 | if len(code) != length { 144 | _, err := dlgs.Error(title, d.printer.Sprintf(localization.CodeInvalidLength, length)+"\n") 145 | if err != nil { 146 | return "", errors.Errorf("write error message: %w", err) 147 | } 148 | continue 149 | } 150 | 151 | return code, nil 152 | // TODO: add tg.AuthSentCodeTypeFlashCall support 153 | default: 154 | return code, nil 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /tg_io/download_test.go: -------------------------------------------------------------------------------- 1 | package tg_io 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "fmt" 8 | "io" 9 | "net" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "testing" 14 | "time" 15 | 16 | "github.com/go-faster/errors" 17 | "github.com/stretchr/testify/assert" 18 | "github.com/stretchr/testify/require" 19 | "go.uber.org/zap/zaptest" 20 | "golang.org/x/sync/errgroup" 21 | "golang.org/x/time/rate" 22 | 23 | "github.com/gotd/td/telegram" 24 | "github.com/gotd/td/telegram/auth" 25 | "github.com/gotd/td/telegram/dcs" 26 | "github.com/gotd/td/telegram/message" 27 | "github.com/gotd/td/telegram/uploader" 28 | "github.com/gotd/td/tg" 29 | 30 | "github.com/gotd/contrib/http_io" 31 | "github.com/gotd/contrib/middleware/floodwait" 32 | "github.com/gotd/contrib/middleware/ratelimit" 33 | "github.com/gotd/contrib/partio" 34 | ) 35 | 36 | const ( 37 | chunk1kb = 1024 38 | ) 39 | 40 | func TestE2E(t *testing.T) { 41 | if os.Getenv("TG_IO_E2E") != "1" { 42 | t.Skip("TG_IO_E2E not set") 43 | } 44 | logger := zaptest.NewLogger(t) 45 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 46 | defer cancel() 47 | 48 | floodWaiter := floodwait.NewWaiter() 49 | 50 | client := telegram.NewClient(telegram.TestAppID, telegram.TestAppHash, telegram.Options{ 51 | DC: 2, 52 | DCList: dcs.Test(), 53 | Logger: logger.Named("client"), 54 | Middlewares: []telegram.Middleware{ 55 | ratelimit.New(rate.Every(100*time.Millisecond), 5), 56 | floodWaiter, 57 | }, 58 | }) 59 | api := tg.NewClient(client) 60 | 61 | handler := func(ctx context.Context) error { 62 | authClient := auth.NewClient(api, rand.Reader, telegram.TestAppID, telegram.TestAppHash) 63 | if err := auth.NewFlow( 64 | auth.Test(rand.Reader, 2), 65 | auth.SendCodeOptions{}, 66 | ).Run(ctx, authClient); err != nil { 67 | return err 68 | } 69 | 70 | const size = chunk1kb*5 + 100 71 | f, err := uploader.NewUploader(api).FromBytes(ctx, "upload.bin", make([]byte, size)) 72 | if err != nil { 73 | return errors.Errorf("upload: %w", err) 74 | } 75 | 76 | mc, err := message.NewSender(api).Self().UploadMedia(ctx, message.File(f)) 77 | if err != nil { 78 | return errors.Errorf("create media: %w", err) 79 | } 80 | 81 | media, ok := mc.(*tg.MessageMediaDocument) 82 | if !ok { 83 | return errors.Errorf("unexpected type: %T", media) 84 | } 85 | 86 | doc, ok := media.Document.AsNotEmpty() 87 | if !ok { 88 | return errors.Errorf("unexpected type: %T", media.Document) 89 | } 90 | 91 | t.Log("Streaming") 92 | u := partio.NewStreamer(NewDownloader(api).ChunkSource(doc.Size, doc.AsInputDocumentFileLocation()), chunk1kb) 93 | buf := new(bytes.Buffer) 94 | 95 | const offset = chunk1kb / 2 96 | if err := u.StreamAt(ctx, offset, buf); err != nil { 97 | return errors.Errorf("stream at %d: %w", offset, err) 98 | } 99 | 100 | t.Log(buf.Len()) 101 | assert.Equal(t, doc.Size-offset, int64(buf.Len())) 102 | 103 | ln, err := net.Listen("tcp", "localhost:0") 104 | if err != nil { 105 | return errors.Errorf("listen: %w", err) 106 | } 107 | defer func() { 108 | _ = ln.Close() 109 | }() 110 | s := http.Server{ 111 | Handler: http_io.NewHandler(u, doc.Size). 112 | WithContentType(doc.MimeType). 113 | WithLog(logger.Named("httpio")), 114 | } 115 | g, ctx := errgroup.WithContext(ctx) 116 | done := make(chan struct{}) 117 | g.Go(func() error { 118 | select { 119 | case <-ctx.Done(): 120 | case <-done: 121 | } 122 | return s.Close() 123 | }) 124 | g.Go(func() error { 125 | if err := s.Serve(ln); err != nil && err != http.ErrServerClosed { 126 | return errors.Errorf("server: %w", err) 127 | } 128 | return nil 129 | }) 130 | g.Go(func() error { 131 | defer close(done) 132 | 133 | requestURL := &url.URL{ 134 | Scheme: "http", 135 | Host: ln.Addr().String(), 136 | } 137 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), http.NoBody) 138 | if err != nil { 139 | return err 140 | } 141 | req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset)) 142 | 143 | res, err := http.DefaultClient.Do(req) 144 | if err != nil { 145 | return errors.Errorf("send GET %q: %w", requestURL, err) 146 | } 147 | defer func() { _ = res.Body.Close() }() 148 | t.Log(res.Status) 149 | 150 | outBuf := new(bytes.Buffer) 151 | if _, err := io.Copy(outBuf, res.Body); err != nil { 152 | return errors.Errorf("read response: %w", err) 153 | } 154 | 155 | t.Log(outBuf.Len()) 156 | 157 | return nil 158 | }) 159 | 160 | return g.Wait() 161 | } 162 | run := func(ctx context.Context) error { 163 | return client.Run(ctx, handler) 164 | } 165 | require.NoError(t, floodWaiter.Run(ctx, run)) 166 | } 167 | -------------------------------------------------------------------------------- /redis/peer_storage.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "strings" 7 | 8 | "github.com/go-faster/errors" 9 | "github.com/go-redis/redis/v8" 10 | "go.uber.org/multierr" 11 | 12 | "github.com/gotd/contrib/storage" 13 | ) 14 | 15 | var _ storage.PeerStorage = PeerStorage{} 16 | 17 | // PeerStorage is a peer storage based on redis. 18 | type PeerStorage struct { 19 | redis *redis.Client 20 | } 21 | 22 | // NewPeerStorage creates new peer storage using redis. 23 | func NewPeerStorage(client *redis.Client) *PeerStorage { 24 | return &PeerStorage{redis: client} 25 | } 26 | 27 | type redisIterator struct { 28 | client *redis.Client 29 | iter *redis.ScanIterator 30 | lastErr error 31 | value storage.Peer 32 | } 33 | 34 | func (p *redisIterator) Close() error { 35 | return nil 36 | } 37 | 38 | func (p *redisIterator) Next(ctx context.Context) bool { 39 | if !p.iter.Next(ctx) { 40 | return false 41 | } 42 | 43 | key := p.iter.Val() 44 | value, err := p.client.Get(ctx, key).Result() 45 | if err != nil { 46 | p.lastErr = errors.Errorf("get %q: %w", key, err) 47 | return false 48 | } 49 | 50 | r := strings.NewReader(value) 51 | if err := json.NewDecoder(r).Decode(&p.value); err != nil { 52 | p.lastErr = errors.Errorf("unmarshal: %w", err) 53 | return false 54 | } 55 | 56 | return true 57 | } 58 | 59 | func (p *redisIterator) Err() error { 60 | return multierr.Append(p.lastErr, p.iter.Err()) 61 | } 62 | 63 | func (p *redisIterator) Value() storage.Peer { 64 | return p.value 65 | } 66 | 67 | // Iterate creates and returns new PeerIterator. 68 | func (s PeerStorage) Iterate(ctx context.Context) (storage.PeerIterator, error) { 69 | var b strings.Builder 70 | b.Grow(len(storage.PeerKeyPrefix) + 1) 71 | b.Write(storage.PeerKeyPrefix) 72 | b.WriteByte('*') 73 | 74 | result := s.redis.Scan(ctx, 0, b.String(), 0) 75 | return &redisIterator{ 76 | client: s.redis, 77 | iter: result.Iterator(), 78 | }, result.Err() 79 | } 80 | 81 | func (s PeerStorage) add(ctx context.Context, associated []string, value storage.Peer) (rerr error) { 82 | data, err := json.Marshal(value) 83 | if err != nil { 84 | return errors.Errorf("marshal: %w", err) 85 | } 86 | id := storage.KeyFromPeer(value).String() 87 | 88 | if len(associated) == 0 { 89 | if err := s.redis.Set(ctx, id, data, 0).Err(); err != nil { 90 | return errors.Errorf("set id <-> data: %w", err) 91 | } 92 | 93 | return nil 94 | } 95 | 96 | tx := s.redis.TxPipeline() 97 | defer func() { 98 | multierr.AppendInto(&rerr, tx.Close()) 99 | }() 100 | 101 | if err := tx.Set(ctx, id, data, 0).Err(); err != nil { 102 | return errors.Errorf("set id <-> data: %w", err) 103 | } 104 | 105 | for _, key := range associated { 106 | if err := tx.Set(ctx, key, id, 0).Err(); err != nil { 107 | return errors.Errorf("set key <-> id: %w", err) 108 | } 109 | } 110 | 111 | if _, err := tx.Exec(ctx); err != nil { 112 | return errors.Errorf("exec: %w", err) 113 | } 114 | 115 | return nil 116 | } 117 | 118 | // Add adds given peer to the storage. 119 | func (s PeerStorage) Add(ctx context.Context, value storage.Peer) error { 120 | return s.add(ctx, value.Keys(), value) 121 | } 122 | 123 | // Find finds peer using given key. 124 | func (s PeerStorage) Find(ctx context.Context, key storage.PeerKey) (storage.Peer, error) { 125 | id := key.String() 126 | 127 | data, err := s.redis.Get(ctx, id).Bytes() 128 | if err != nil { 129 | if errors.Is(err, redis.Nil) { 130 | return storage.Peer{}, storage.ErrPeerNotFound 131 | } 132 | return storage.Peer{}, errors.Errorf("get %q: %w", key, err) 133 | } 134 | 135 | var b storage.Peer 136 | if err := json.Unmarshal(data, &b); err != nil { 137 | return storage.Peer{}, errors.Errorf("unmarshal: %w", err) 138 | } 139 | 140 | return b, nil 141 | } 142 | 143 | // Assign adds given peer to the storage and associate it to the given key. 144 | func (s PeerStorage) Assign(ctx context.Context, key string, value storage.Peer) (rerr error) { 145 | return s.add(ctx, append(value.Keys(), key), value) 146 | } 147 | 148 | // Resolve finds peer using associated key. 149 | func (s PeerStorage) Resolve(ctx context.Context, key string) (storage.Peer, error) { 150 | // Find id by domain. 151 | id, err := s.redis.Get(ctx, key).Result() 152 | if err != nil { 153 | if errors.Is(err, redis.Nil) { 154 | return storage.Peer{}, storage.ErrPeerNotFound 155 | } 156 | return storage.Peer{}, errors.Errorf("get %q: %w", key, err) 157 | } 158 | 159 | // Find object by id. 160 | data, err := s.redis.Get(ctx, id).Bytes() 161 | if err != nil { 162 | if errors.Is(err, redis.Nil) { 163 | return storage.Peer{}, storage.ErrPeerNotFound 164 | } 165 | return storage.Peer{}, errors.Errorf("get %q: %w", id, err) 166 | } 167 | 168 | var b storage.Peer 169 | if err := json.Unmarshal(data, &b); err != nil { 170 | return storage.Peer{}, errors.Errorf("unmarshal: %w", err) 171 | } 172 | 173 | return b, nil 174 | } 175 | -------------------------------------------------------------------------------- /bbolt/peer_storage.go: -------------------------------------------------------------------------------- 1 | package bbolt 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | 8 | "github.com/go-faster/errors" 9 | "go.etcd.io/bbolt" 10 | 11 | "github.com/gotd/contrib/storage" 12 | ) 13 | 14 | var _ storage.PeerStorage = PeerStorage{} 15 | 16 | // PeerStorage is a peer storage based on pebble. 17 | type PeerStorage struct { 18 | bbolt *bbolt.DB 19 | bucket []byte 20 | } 21 | 22 | // NewPeerStorage creates new peer storage using bbolt. 23 | func NewPeerStorage(db *bbolt.DB, bucket []byte) *PeerStorage { 24 | return &PeerStorage{bbolt: db, bucket: bucket} 25 | } 26 | 27 | type bboltIterator struct { 28 | tx *bbolt.Tx 29 | iter *bbolt.Cursor 30 | lastErr error 31 | value storage.Peer 32 | } 33 | 34 | func (p *bboltIterator) Close() error { 35 | return p.tx.Rollback() 36 | } 37 | 38 | func (p *bboltIterator) Next(ctx context.Context) bool { 39 | Next: 40 | k, v := p.iter.Next() 41 | if v == nil { 42 | return false 43 | } 44 | 45 | for !bytes.HasPrefix(k, storage.PeerKeyPrefix) { 46 | k, v = p.iter.Next() 47 | if v == nil { 48 | return false 49 | } 50 | } 51 | 52 | if err := json.Unmarshal(v, &p.value); err != nil { 53 | if errors.Is(err, storage.ErrPeerUnmarshalMustInvalidate) { 54 | goto Next // skip 55 | } 56 | p.lastErr = errors.Wrap(err, "unmarshal") 57 | return false 58 | } 59 | 60 | return true 61 | } 62 | 63 | func (p *bboltIterator) Err() error { 64 | return p.lastErr 65 | } 66 | 67 | func (p *bboltIterator) Value() storage.Peer { 68 | return p.value 69 | } 70 | 71 | // Iterate creates and returns new PeerIterator. 72 | func (s PeerStorage) Iterate(ctx context.Context) (storage.PeerIterator, error) { 73 | tx, err := s.bbolt.Begin(false) 74 | if err != nil { 75 | return nil, errors.Errorf("create tx: %w", err) 76 | } 77 | 78 | bucket := tx.Bucket(s.bucket) 79 | if bucket == nil { 80 | return nil, errors.Errorf("bucket %q does not exist", s.bucket) 81 | } 82 | 83 | cur := bucket.Cursor() 84 | cur.Seek(storage.PeerKeyPrefix) 85 | cur.Prev() 86 | return &bboltIterator{ 87 | tx: tx, 88 | iter: cur, 89 | }, nil 90 | } 91 | 92 | func (s PeerStorage) add(associated []string, value storage.Peer) (err error) { 93 | err = s.bbolt.Batch(func(tx *bbolt.Tx) error { 94 | bucket, err := tx.CreateBucketIfNotExists(s.bucket) 95 | if err != nil { 96 | return errors.Errorf("create bucket: %w", err) 97 | } 98 | 99 | data, err := json.Marshal(value) 100 | if err != nil { 101 | return errors.Errorf("marshal: %w", err) 102 | } 103 | id := storage.KeyFromPeer(value).Bytes(nil) 104 | 105 | if err := bucket.Put(id, data); err != nil { 106 | return errors.Errorf("set id <-> data: %w", err) 107 | } 108 | 109 | for _, key := range associated { 110 | if err := bucket.Put([]byte(key), id); err != nil { 111 | return errors.Errorf("set key <-> id: %w", err) 112 | } 113 | } 114 | 115 | return nil 116 | }) 117 | return 118 | } 119 | 120 | // Add adds given peer to the storage. 121 | func (s PeerStorage) Add(ctx context.Context, value storage.Peer) error { 122 | return s.add(value.Keys(), value) 123 | } 124 | 125 | // Find finds peer using given key. 126 | func (s PeerStorage) Find(ctx context.Context, key storage.PeerKey) (p storage.Peer, rerr error) { 127 | rerr = s.bbolt.View(func(tx *bbolt.Tx) error { 128 | bucket := tx.Bucket(s.bucket) 129 | if bucket == nil { 130 | return errors.Errorf("bucket %q does not exist", s.bucket) 131 | } 132 | 133 | data := bucket.Get(key.Bytes(nil)) 134 | if data == nil { 135 | return storage.ErrPeerNotFound 136 | } 137 | 138 | if err := json.Unmarshal(data, &p); err != nil { 139 | if errors.Is(err, storage.ErrPeerUnmarshalMustInvalidate) { 140 | return storage.ErrPeerNotFound 141 | } 142 | return errors.Errorf("unmarshal: %w", err) 143 | } 144 | return nil 145 | }) 146 | return 147 | } 148 | 149 | // Assign adds given peer to the storage and associate it to the given key. 150 | func (s PeerStorage) Assign(ctx context.Context, key string, value storage.Peer) error { 151 | return s.add(append(value.Keys(), key), value) 152 | } 153 | 154 | // Resolve finds peer using associated key. 155 | func (s PeerStorage) Resolve(ctx context.Context, key string) (p storage.Peer, rerr error) { 156 | rerr = s.bbolt.View(func(tx *bbolt.Tx) error { 157 | bucket := tx.Bucket(s.bucket) 158 | if bucket == nil { 159 | return errors.Errorf("bucket %q does not exist", s.bucket) 160 | } 161 | 162 | id := bucket.Get([]byte(key)) 163 | if id == nil { 164 | return storage.ErrPeerNotFound 165 | } 166 | 167 | data := bucket.Get(id) 168 | if data == nil { 169 | return storage.ErrPeerNotFound 170 | } 171 | 172 | if err := json.Unmarshal(data, &p); err != nil { 173 | if errors.Is(err, storage.ErrPeerUnmarshalMustInvalidate) { 174 | return storage.ErrPeerNotFound 175 | } 176 | return errors.Errorf("unmarshal: %w", err) 177 | } 178 | return nil 179 | }) 180 | return 181 | } 182 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gotd/contrib 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/beevik/ntp v1.5.0 9 | github.com/cenkalti/backoff/v4 v4.3.0 10 | github.com/cockroachdb/pebble v1.1.5 11 | github.com/gen2brain/dlgs v0.0.0-20211108104213-bade24837f0b 12 | github.com/go-faster/errors v0.7.1 13 | github.com/go-faster/jx v1.1.0 14 | github.com/go-redis/redis/v8 v8.11.5 15 | github.com/gotd/neo v0.1.5 16 | github.com/gotd/td v0.131.0 17 | github.com/hashicorp/vault/api v1.22.0 18 | github.com/minio/minio-go/v7 v7.0.95 19 | github.com/prometheus/client_golang v1.23.2 20 | github.com/stretchr/testify v1.11.1 21 | go.etcd.io/bbolt v1.4.3 22 | go.opentelemetry.io/otel v1.38.0 23 | go.opentelemetry.io/otel/metric v1.38.0 24 | go.opentelemetry.io/otel/trace v1.38.0 25 | go.uber.org/atomic v1.11.0 26 | go.uber.org/multierr v1.11.0 27 | go.uber.org/zap v1.27.0 28 | golang.org/x/sync v0.17.0 29 | golang.org/x/term v0.35.0 30 | golang.org/x/text v0.29.0 31 | golang.org/x/time v0.13.0 32 | ) 33 | 34 | require ( 35 | github.com/DataDog/zstd v1.4.5 // indirect 36 | github.com/beorn7/perks v1.0.1 // indirect 37 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 38 | github.com/cockroachdb/errors v1.11.3 // indirect 39 | github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect 40 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect 41 | github.com/cockroachdb/redact v1.1.5 // indirect 42 | github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect 43 | github.com/coder/websocket v1.8.13 // indirect 44 | github.com/davecgh/go-spew v1.1.1 // indirect 45 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 46 | github.com/dlclark/regexp2 v1.11.5 // indirect 47 | github.com/dustin/go-humanize v1.0.1 // indirect 48 | github.com/fatih/color v1.18.0 // indirect 49 | github.com/getsentry/sentry-go v0.27.0 // indirect 50 | github.com/ghodss/yaml v1.0.0 // indirect 51 | github.com/go-faster/xor v1.0.0 // indirect 52 | github.com/go-faster/yaml v0.4.6 // indirect 53 | github.com/go-ini/ini v1.67.0 // indirect 54 | github.com/go-jose/go-jose/v4 v4.1.1 // indirect 55 | github.com/goccy/go-json v0.10.5 // indirect 56 | github.com/gogo/protobuf v1.3.2 // indirect 57 | github.com/golang/snappy v0.0.4 // indirect 58 | github.com/google/uuid v1.6.0 // indirect 59 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect 60 | github.com/gotd/ige v0.2.2 // indirect 61 | github.com/hashicorp/errwrap v1.1.0 // indirect 62 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 63 | github.com/hashicorp/go-multierror v1.1.1 // indirect 64 | github.com/hashicorp/go-retryablehttp v0.7.8 // indirect 65 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 66 | github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect 67 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 68 | github.com/hashicorp/go-sockaddr v1.0.7 // indirect 69 | github.com/hashicorp/hcl v1.0.1-vault-7 // indirect 70 | github.com/klauspost/compress v1.18.0 // indirect 71 | github.com/klauspost/cpuid/v2 v2.2.11 // indirect 72 | github.com/kr/pretty v0.3.1 // indirect 73 | github.com/kr/text v0.2.0 // indirect 74 | github.com/mattn/go-colorable v0.1.14 // indirect 75 | github.com/mattn/go-isatty v0.0.20 // indirect 76 | github.com/minio/crc64nvme v1.0.2 // indirect 77 | github.com/minio/md5-simd v1.1.2 // indirect 78 | github.com/mitchellh/go-homedir v1.1.0 // indirect 79 | github.com/mitchellh/mapstructure v1.5.0 // indirect 80 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 81 | github.com/ogen-go/ogen v1.14.0 // indirect 82 | github.com/philhofer/fwd v1.2.0 // indirect 83 | github.com/pkg/errors v0.9.1 // indirect 84 | github.com/pmezard/go-difflib v1.0.0 // indirect 85 | github.com/prometheus/client_model v0.6.2 // indirect 86 | github.com/prometheus/common v0.66.1 // indirect 87 | github.com/prometheus/procfs v0.16.1 // indirect 88 | github.com/rogpeppe/go-internal v1.14.1 // indirect 89 | github.com/rs/xid v1.6.0 // indirect 90 | github.com/ryanuber/go-glob v1.0.0 // indirect 91 | github.com/segmentio/asm v1.2.0 // indirect 92 | github.com/tinylib/msgp v1.3.0 // indirect 93 | go.yaml.in/yaml/v2 v2.4.2 // indirect 94 | golang.org/x/crypto v0.42.0 // indirect 95 | golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect 96 | golang.org/x/mod v0.27.0 // indirect 97 | golang.org/x/net v0.44.0 // indirect 98 | golang.org/x/sys v0.36.0 // indirect 99 | golang.org/x/tools v0.36.0 // indirect 100 | google.golang.org/protobuf v1.36.8 // indirect 101 | gopkg.in/yaml.v2 v2.4.0 // indirect 102 | gopkg.in/yaml.v3 v3.0.1 // indirect 103 | rsc.io/qr v0.2.0 // indirect 104 | ) 105 | 106 | replace ( 107 | github.com/coreos/etcd v3.3.10+incompatible => github.com/coreos/etcd v3.3.25+incompatible 108 | github.com/dgrijalva/jwt-go v3.2.0+incompatible => github.com/form3tech-oss/jwt-go v3.2.2+incompatible 109 | github.com/gogo/protobuf v1.3.1 => github.com/gogo/protobuf v1.3.2 110 | github.com/gorilla/websocket v1.4.0 => github.com/gorilla/websocket v1.4.2 111 | ) 112 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: E2E 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | workflow_dispatch: 10 | schedule: 11 | - cron: '0 0 * * *' 12 | 13 | jobs: 14 | vault: 15 | runs-on: ubuntu-latest 16 | services: 17 | vault: 18 | image: "hashicorp/vault" 19 | ports: 20 | - 8200:8200 21 | env: 22 | VAULT_DEV_ROOT_TOKEN_ID: testtoken 23 | steps: 24 | - uses: actions/checkout@v5.0.0 25 | - name: Install Go 26 | uses: actions/setup-go@v5.5.0 27 | with: 28 | go-version: 1.21.x 29 | cache: false 30 | 31 | - name: Get Go environment 32 | id: go-env 33 | run: | 34 | echo "::set-output name=cache::$(go env GOCACHE)" 35 | echo "::set-output name=modcache::$(go env GOMODCACHE)" 36 | - name: Set up cache 37 | uses: actions/cache@v4.3.0 38 | with: 39 | path: | 40 | ${{ steps.go-env.outputs.cache }} 41 | ${{ steps.go-env.outputs.modcache }} 42 | key: check-mod-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 43 | restore-keys: | 44 | check-mod-${{ runner.os }}-go- 45 | 46 | - name: Run tests 47 | env: 48 | VAULT_ADDR: "http://localhost:8200" 49 | VAULT_TOKEN: "testtoken" 50 | run: make e2e_vault_test 51 | 52 | - name: Send coverage 53 | uses: codecov/codecov-action@v5.5.1 54 | with: 55 | file: profile.out 56 | 57 | redis: 58 | runs-on: ubuntu-latest 59 | services: 60 | redis: 61 | image: "redis:latest" 62 | ports: 63 | - 6379:6379 64 | steps: 65 | - uses: actions/checkout@v5.0.0 66 | - name: Install Go 67 | uses: actions/setup-go@v5.5.0 68 | with: 69 | go-version: 1.21.x 70 | cache: false 71 | 72 | - name: Get Go environment 73 | id: go-env 74 | run: | 75 | echo "::set-output name=cache::$(go env GOCACHE)" 76 | echo "::set-output name=modcache::$(go env GOMODCACHE)" 77 | - name: Set up cache 78 | uses: actions/cache@v4.3.0 79 | with: 80 | path: | 81 | ${{ steps.go-env.outputs.cache }} 82 | ${{ steps.go-env.outputs.modcache }} 83 | key: check-mod-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 84 | restore-keys: | 85 | check-mod-${{ runner.os }}-go- 86 | 87 | - name: Run tests 88 | env: 89 | REDIS_ADDR: "localhost:6379" 90 | run: make e2e_redis_test 91 | 92 | - name: Send coverage 93 | uses: codecov/codecov-action@v5.5.1 94 | with: 95 | file: profile.out 96 | 97 | s3: 98 | runs-on: ubuntu-latest 99 | steps: 100 | - uses: actions/checkout@v5.0.0 101 | - name: Install Go 102 | uses: actions/setup-go@v5.5.0 103 | with: 104 | go-version: 1.21.x 105 | cache: false 106 | 107 | - name: Get Go environment 108 | id: go-env 109 | run: | 110 | echo "::set-output name=cache::$(go env GOCACHE)" 111 | echo "::set-output name=modcache::$(go env GOMODCACHE)" 112 | - name: Set up cache 113 | uses: actions/cache@v4.3.0 114 | with: 115 | path: | 116 | ${{ steps.go-env.outputs.cache }} 117 | ${{ steps.go-env.outputs.modcache }} 118 | key: check-mod-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 119 | restore-keys: | 120 | check-mod-${{ runner.os }}-go- 121 | 122 | - name: Setup minio 123 | run: | 124 | docker run -d -p 9000:9000 --name minio \ 125 | -e "MINIO_ACCESS_KEY=minioadmin" \ 126 | -e "MINIO_SECRET_KEY=minioadmin" \ 127 | -v /tmp/data:/data \ 128 | -v /tmp/config:/root/.minio \ 129 | minio/minio server /data 130 | 131 | - name: Run tests 132 | env: 133 | MINIO_ACCESS_KEY: minioadmin 134 | MINIO_SECRET_KEY: minioadmin 135 | S3_ADDR: "localhost:9000" 136 | run: make e2e_s3_test 137 | 138 | - name: Send coverage 139 | uses: codecov/codecov-action@v5.5.1 140 | with: 141 | file: profile.out 142 | 143 | tg_io: 144 | runs-on: ubuntu-latest 145 | steps: 146 | - uses: actions/checkout@v5.0.0 147 | - name: Install Go 148 | uses: actions/setup-go@v5.5.0 149 | with: 150 | go-version: 1.21.x 151 | cache: false 152 | 153 | - name: Get Go environment 154 | id: go-env 155 | run: | 156 | echo "::set-output name=cache::$(go env GOCACHE)" 157 | echo "::set-output name=modcache::$(go env GOMODCACHE)" 158 | - name: Set up cache 159 | uses: actions/cache@v4.3.0 160 | with: 161 | path: | 162 | ${{ steps.go-env.outputs.cache }} 163 | ${{ steps.go-env.outputs.modcache }} 164 | key: check-mod-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 165 | restore-keys: | 166 | check-mod-${{ runner.os }}-go- 167 | - name: Run tests 168 | env: 169 | TG_IO_E2E: 0 170 | run: make e2e_tg_io_test 171 | 172 | - name: Send coverage 173 | uses: codecov/codecov-action@v5.5.1 174 | with: 175 | file: profile.out -------------------------------------------------------------------------------- /pebble/peer_storage.go: -------------------------------------------------------------------------------- 1 | package pebble 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | 8 | "github.com/cockroachdb/pebble" 9 | "github.com/go-faster/errors" 10 | "go.uber.org/multierr" 11 | 12 | "github.com/gotd/contrib/storage" 13 | ) 14 | 15 | var _ storage.PeerStorage = PeerStorage{} 16 | 17 | // PeerStorage is a peer storage based on pebble. 18 | type PeerStorage struct { 19 | pebble *pebble.DB 20 | writeOpts *pebble.WriteOptions 21 | } 22 | 23 | // NewPeerStorage creates new peer storage using pebble. 24 | func NewPeerStorage(db *pebble.DB) *PeerStorage { 25 | return &PeerStorage{pebble: db} 26 | } 27 | 28 | // WithWriteOptions sets pebble's write options for write operations. 29 | func (s *PeerStorage) WithWriteOptions(writeOpts *pebble.WriteOptions) *PeerStorage { 30 | s.writeOpts = writeOpts 31 | return s 32 | } 33 | 34 | type pebbleIterator struct { 35 | snap *pebble.Snapshot 36 | iter *pebble.Iterator 37 | lastErr error 38 | value storage.Peer 39 | } 40 | 41 | func (p *pebbleIterator) Close() error { 42 | return multierr.Append(p.iter.Close(), p.snap.Close()) 43 | } 44 | 45 | func (p *pebbleIterator) Next(ctx context.Context) bool { 46 | if !p.iter.Valid() { 47 | return false 48 | } 49 | 50 | for !bytes.HasPrefix(p.iter.Key(), storage.PeerKeyPrefix) { 51 | if !p.iter.Next() { 52 | return false 53 | } 54 | } 55 | 56 | if err := json.Unmarshal(p.iter.Value(), &p.value); err != nil { 57 | p.lastErr = errors.Errorf("unmarshal: %w", err) 58 | return false 59 | } 60 | 61 | p.iter.Next() 62 | return true 63 | } 64 | 65 | func (p *pebbleIterator) Err() error { 66 | return p.lastErr 67 | } 68 | 69 | func (p *pebbleIterator) Value() storage.Peer { 70 | return p.value 71 | } 72 | 73 | func keyUpperBound(b []byte) []byte { 74 | end := make([]byte, len(b)) 75 | copy(end, b) 76 | for i := len(end) - 1; i >= 0; i-- { 77 | end[i]++ 78 | if end[i] != 0 { 79 | return end[:i+1] 80 | } 81 | } 82 | return nil // no upper-bound 83 | } 84 | 85 | func prefixIterOptions(prefix []byte) *pebble.IterOptions { 86 | return &pebble.IterOptions{ 87 | LowerBound: prefix, 88 | UpperBound: keyUpperBound(prefix), 89 | } 90 | } 91 | 92 | // Iterate creates and returns new PeerIterator. 93 | func (s PeerStorage) Iterate(ctx context.Context) (storage.PeerIterator, error) { 94 | snap := s.pebble.NewSnapshot() 95 | iter, err := snap.NewIter(prefixIterOptions(storage.PeerKeyPrefix)) 96 | if err != nil { 97 | _ = snap.Close() 98 | return nil, errors.Errorf("new iter: %w", err) 99 | } 100 | iter.First() 101 | 102 | return &pebbleIterator{ 103 | snap: snap, 104 | iter: iter, 105 | }, nil 106 | } 107 | 108 | func (s PeerStorage) add(associated []string, value storage.Peer) (rerr error) { 109 | data, err := json.Marshal(value) 110 | if err != nil { 111 | return errors.Errorf("marshal: %w", err) 112 | } 113 | id := storage.KeyFromPeer(value).Bytes(nil) 114 | 115 | b := s.pebble.NewBatch() 116 | defer func() { 117 | multierr.AppendInto(&rerr, b.Close()) 118 | }() 119 | 120 | set := b.SetDeferred(len(id), len(data)) 121 | copy(set.Key, id) 122 | copy(set.Value, data) 123 | _ = set.Finish() 124 | 125 | for _, key := range associated { 126 | deferred := b.SetDeferred(len(key), len(id)) 127 | copy(deferred.Key, key) 128 | copy(deferred.Value, id) 129 | _ = deferred.Finish() 130 | } 131 | 132 | if err := b.Commit(nil); err != nil { 133 | return errors.Errorf("commit: %w", err) 134 | } 135 | 136 | return nil 137 | } 138 | 139 | // Add adds given peer to the storage. 140 | func (s PeerStorage) Add(ctx context.Context, value storage.Peer) (rerr error) { 141 | return s.add(value.Keys(), value) 142 | } 143 | 144 | // Find finds peer using given key. 145 | func (s PeerStorage) Find(ctx context.Context, key storage.PeerKey) (_ storage.Peer, rerr error) { 146 | id := key.Bytes(nil) 147 | 148 | data, closer, err := s.pebble.Get(id) 149 | if err != nil { 150 | if errors.Is(err, pebble.ErrNotFound) { 151 | return storage.Peer{}, storage.ErrPeerNotFound 152 | } 153 | return storage.Peer{}, errors.Errorf("get %q: %w", id, err) 154 | } 155 | defer func() { 156 | multierr.AppendInto(&rerr, closer.Close()) 157 | }() 158 | 159 | var b storage.Peer 160 | if err := json.Unmarshal(data, &b); err != nil { 161 | if errors.Is(err, storage.ErrPeerUnmarshalMustInvalidate) { 162 | return storage.Peer{}, storage.ErrPeerNotFound 163 | } 164 | return storage.Peer{}, errors.Errorf("unmarshal: %w", err) 165 | } 166 | 167 | return b, nil 168 | } 169 | 170 | // Assign adds given peer to the storage and associate it to the given key. 171 | func (s PeerStorage) Assign(ctx context.Context, key string, value storage.Peer) (rerr error) { 172 | return s.add(append(value.Keys(), key), value) 173 | } 174 | 175 | // Resolve finds peer using associated key. 176 | func (s PeerStorage) Resolve(ctx context.Context, key string) (_ storage.Peer, rerr error) { 177 | // Create database snapshot. 178 | snap := s.pebble.NewSnapshot() 179 | defer func() { 180 | multierr.AppendInto(&rerr, snap.Close()) 181 | }() 182 | 183 | // Find id by key. 184 | id, idCloser, err := snap.Get([]byte(key)) 185 | if err != nil { 186 | if errors.Is(err, pebble.ErrNotFound) { 187 | return storage.Peer{}, storage.ErrPeerNotFound 188 | } 189 | return storage.Peer{}, errors.Errorf("get %q: %w", key, err) 190 | } 191 | defer func() { 192 | multierr.AppendInto(&rerr, idCloser.Close()) 193 | }() 194 | 195 | // Find object by id. 196 | data, dataCloser, err := snap.Get(id) 197 | if err != nil { 198 | if errors.Is(err, pebble.ErrNotFound) { 199 | return storage.Peer{}, storage.ErrPeerNotFound 200 | } 201 | return storage.Peer{}, errors.Errorf("get %q: %w", id, err) 202 | } 203 | defer func() { 204 | multierr.AppendInto(&rerr, dataCloser.Close()) 205 | }() 206 | 207 | var b storage.Peer 208 | if err := json.Unmarshal(data, &b); err != nil { 209 | if errors.Is(err, storage.ErrPeerUnmarshalMustInvalidate) { 210 | return storage.Peer{}, storage.ErrPeerNotFound 211 | } 212 | return storage.Peer{}, errors.Errorf("unmarshal: %w", err) 213 | } 214 | 215 | return b, nil 216 | } 217 | -------------------------------------------------------------------------------- /middleware/floodwait/waiter.go: -------------------------------------------------------------------------------- 1 | package floodwait 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/go-faster/errors" 8 | "go.uber.org/atomic" 9 | "golang.org/x/sync/errgroup" 10 | 11 | "github.com/gotd/td/telegram" 12 | "github.com/gotd/td/tg" 13 | 14 | "github.com/gotd/td/bin" 15 | "github.com/gotd/td/clock" 16 | "github.com/gotd/td/tgerr" 17 | ) 18 | 19 | const ( 20 | defaultTick = time.Millisecond 21 | defaultMaxWait = time.Minute 22 | defaultMaxRetries = 5 23 | ) 24 | 25 | // Waiter is a tg.Invoker that handles flood wait errors on underlying invoker. 26 | // 27 | // This implementation uses a request scheduler and is more suitable for long-running 28 | // programs with high level of concurrency and parallelism. 29 | // 30 | // You should use Waiter if unsure which waiter implementation to use. 31 | // 32 | // See SimpleWaiter for a simple timer-based implementation. 33 | type Waiter struct { 34 | clock clock.Clock 35 | sch *scheduler 36 | 37 | running atomic.Bool 38 | tick time.Duration 39 | maxWait time.Duration 40 | maxRetries int 41 | onWait func(ctx context.Context, wait FloodWait) 42 | } 43 | 44 | // FloodWait event. 45 | type FloodWait struct { 46 | Duration time.Duration 47 | } 48 | 49 | // NewWaiter returns a new invoker that waits on the flood wait errors. 50 | // 51 | // NB: You MUST use Run method. Example: 52 | // 53 | // if err := waiter.Run(ctx, func(ctx context.Context) error { 54 | // // Client should be started after waiter. 55 | // return client.Run(ctx, handler) 56 | // }); err != nil { 57 | // return errors.Wrap(err, "run client") 58 | // } 59 | func NewWaiter() *Waiter { 60 | return &Waiter{ 61 | clock: clock.System, 62 | sch: newScheduler(clock.System, time.Second), 63 | tick: defaultTick, 64 | maxWait: defaultMaxWait, 65 | maxRetries: defaultMaxRetries, 66 | onWait: func(ctx context.Context, wait FloodWait) {}, 67 | } 68 | } 69 | 70 | // WithCallback sets callback for flood wait event. 71 | func (w *Waiter) WithCallback(f func(ctx context.Context, wait FloodWait)) *Waiter { 72 | w = w.clone() 73 | w.onWait = f 74 | return w 75 | } 76 | 77 | // clone returns a copy of the Waiter. 78 | func (w *Waiter) clone() *Waiter { 79 | return &Waiter{ 80 | clock: w.clock, 81 | sch: w.sch, 82 | tick: w.tick, 83 | maxWait: w.maxWait, 84 | maxRetries: w.maxRetries, 85 | onWait: w.onWait, 86 | } 87 | } 88 | 89 | // WithClock sets clock to use. Default is to use system clock. 90 | func (w *Waiter) WithClock(c clock.Clock) *Waiter { 91 | w = w.clone() 92 | w.clock = c 93 | return w 94 | } 95 | 96 | // WithMaxWait limits wait time per attempt. Waiter will return an error if flood wait 97 | // time exceeds that limit. Default is to wait at most a minute. 98 | // 99 | // To limit total wait time use a context.Context with timeout or deadline set. 100 | func (w *Waiter) WithMaxWait(m time.Duration) *Waiter { 101 | w = w.clone() 102 | w.maxWait = m 103 | return w 104 | } 105 | 106 | // WithMaxRetries sets max number of retries before giving up. Default is to retry at most 5 times. 107 | func (w *Waiter) WithMaxRetries(m int) *Waiter { 108 | w = w.clone() 109 | w.maxRetries = m 110 | return w 111 | } 112 | 113 | // WithTick sets gather tick interval for Waiter. Default is 1ms. 114 | func (w *Waiter) WithTick(t time.Duration) *Waiter { 115 | w = w.clone() 116 | if t <= 0 { 117 | t = time.Nanosecond 118 | } 119 | w.tick = t 120 | return w 121 | } 122 | 123 | // Run runs send loop. 124 | // 125 | // Example: 126 | // 127 | // if err := waiter.Run(ctx, func(ctx context.Context) error { 128 | // // Client should be started after waiter. 129 | // return client.Run(ctx, handler) 130 | // }); err != nil { 131 | // return errors.Wrap(err, "run client") 132 | // } 133 | func (w *Waiter) Run(ctx context.Context, f func(ctx context.Context) error) (err error) { 134 | w.running.Store(true) 135 | defer w.running.Store(false) 136 | 137 | ctx, cancel := context.WithCancel(ctx) 138 | wg, ctx := errgroup.WithContext(ctx) 139 | wg.Go(func() error { 140 | defer cancel() 141 | return f(ctx) 142 | }) 143 | wg.Go(func() error { 144 | ticker := w.clock.Ticker(w.tick) 145 | defer ticker.Stop() 146 | 147 | var requests []scheduled 148 | for { 149 | select { 150 | case <-ticker.C(): 151 | requests = w.sch.gather(requests[:0]) 152 | if len(requests) < 1 { 153 | continue 154 | } 155 | 156 | for _, s := range requests { 157 | ret, err := w.send(s) 158 | if ret { 159 | select { 160 | case s.request.result <- err: 161 | default: 162 | } 163 | } 164 | } 165 | case <-ctx.Done(): 166 | return nil 167 | } 168 | } 169 | }) 170 | 171 | return wg.Wait() 172 | } 173 | 174 | func (w *Waiter) send(s scheduled) (bool, error) { 175 | err := s.request.next.Invoke(s.request.ctx, s.request.input, s.request.output) 176 | 177 | d, ok := tgerr.AsFloodWait(err) 178 | if !ok { 179 | w.sch.nice(s.request.key) 180 | return true, err 181 | } 182 | 183 | // Notify about flood wait. 184 | w.onWait(s.request.ctx, FloodWait{ 185 | Duration: d, 186 | }) 187 | 188 | s.request.retry++ 189 | 190 | if v := w.maxRetries; v != 0 && s.request.retry > v { 191 | return true, errors.Errorf("flood wait retry limit exceeded (%d > %d): %w", s.request.retry, v, err) 192 | } 193 | 194 | if d < time.Second { 195 | d = time.Second 196 | } 197 | if v := w.maxWait; v != 0 && d > v { 198 | return true, errors.Errorf("flood wait argument is too big (%v > %v): %w", d, v, err) 199 | } 200 | 201 | w.sch.flood(s.request, d) 202 | return false, nil 203 | } 204 | 205 | // Handle implements telegram.Middleware. 206 | func (w *Waiter) Handle(next tg.Invoker) telegram.InvokeFunc { 207 | return func(ctx context.Context, input bin.Encoder, output bin.Decoder) error { 208 | if !w.running.Load() { 209 | // Return explicit error if waiter is not running. 210 | return errors.New("the Waiter middleware is not running: Run(ctx) method is not called or exited") 211 | } 212 | select { 213 | case err := <-w.sch.new(ctx, input, output, next): 214 | return err 215 | case <-ctx.Done(): 216 | return ctx.Err() 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /bbolt/state_storage.go: -------------------------------------------------------------------------------- 1 | package bbolt 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "fmt" 7 | 8 | bolt "go.etcd.io/bbolt" 9 | 10 | "github.com/gotd/td/telegram/updates" 11 | ) 12 | 13 | func i2b(v int) []byte { b := make([]byte, 8); binary.LittleEndian.PutUint64(b, uint64(v)); return b } 14 | 15 | func b2i(b []byte) int { return int(binary.LittleEndian.Uint64(b)) } 16 | 17 | func i642b(v int64) []byte { 18 | b := make([]byte, 16) 19 | binary.LittleEndian.PutUint64(b, uint64(v)) 20 | return b 21 | } 22 | 23 | func b2i64(b []byte) int64 { return int64(binary.LittleEndian.Uint64(b)) } 24 | 25 | var _ updates.StateStorage = (*State)(nil) 26 | 27 | // State is updates.StateStorage implementation using bbolt. 28 | type State struct { 29 | db *bolt.DB 30 | } 31 | 32 | // NewStateStorage creates new state storage over bbolt. 33 | // 34 | // Caller is responsible for db.Close() invocation. 35 | func NewStateStorage(db *bolt.DB) *State { return &State{db} } 36 | 37 | func (s *State) GetState(_ context.Context, userID int64) (state updates.State, found bool, err error) { 38 | tx, err := s.db.Begin(false) 39 | if err != nil { 40 | return updates.State{}, false, err 41 | } 42 | defer func() { _ = tx.Rollback() }() 43 | 44 | user := tx.Bucket(i642b(userID)) 45 | if user == nil { 46 | return updates.State{}, false, nil 47 | } 48 | 49 | stateBucket := user.Bucket([]byte("state")) 50 | if stateBucket == nil { 51 | return updates.State{}, false, nil 52 | } 53 | 54 | var ( 55 | pts = stateBucket.Get([]byte("pts")) 56 | qts = stateBucket.Get([]byte("qts")) 57 | date = stateBucket.Get([]byte("date")) 58 | seq = stateBucket.Get([]byte("seq")) 59 | ) 60 | 61 | if pts == nil || qts == nil || date == nil || seq == nil { 62 | return updates.State{}, false, nil 63 | } 64 | 65 | return updates.State{ 66 | Pts: b2i(pts), 67 | Qts: b2i(qts), 68 | Date: b2i(date), 69 | Seq: b2i(seq), 70 | }, true, nil 71 | } 72 | 73 | func (s *State) SetState(_ context.Context, userID int64, state updates.State) error { 74 | return s.db.Update(func(tx *bolt.Tx) error { 75 | user, err := tx.CreateBucketIfNotExists(i642b(userID)) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | b, err := user.CreateBucketIfNotExists([]byte("state")) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | check := func(e error) { 86 | if err != nil { 87 | return 88 | } 89 | err = e 90 | } 91 | 92 | check(b.Put([]byte("pts"), i2b(state.Pts))) 93 | check(b.Put([]byte("qts"), i2b(state.Qts))) 94 | check(b.Put([]byte("date"), i2b(state.Date))) 95 | check(b.Put([]byte("seq"), i2b(state.Seq))) 96 | return err 97 | }) 98 | } 99 | 100 | func (s *State) SetPts(_ context.Context, userID int64, pts int) error { 101 | return s.db.Update(func(tx *bolt.Tx) error { 102 | user, err := tx.CreateBucketIfNotExists(i642b(userID)) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | state := user.Bucket([]byte("state")) 108 | if state == nil { 109 | return fmt.Errorf("state not found") 110 | } 111 | return state.Put([]byte("pts"), i2b(pts)) 112 | }) 113 | } 114 | 115 | func (s *State) SetQts(_ context.Context, userID int64, qts int) error { 116 | return s.db.Update(func(tx *bolt.Tx) error { 117 | user, err := tx.CreateBucketIfNotExists(i642b(userID)) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | state := user.Bucket([]byte("state")) 123 | if state == nil { 124 | return fmt.Errorf("state not found") 125 | } 126 | return state.Put([]byte("qts"), i2b(qts)) 127 | }) 128 | } 129 | 130 | func (s *State) SetDate(_ context.Context, userID int64, date int) error { 131 | return s.db.Update(func(tx *bolt.Tx) error { 132 | user, err := tx.CreateBucketIfNotExists(i642b(userID)) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | state := user.Bucket([]byte("state")) 138 | if state == nil { 139 | return fmt.Errorf("state not found") 140 | } 141 | return state.Put([]byte("date"), i2b(date)) 142 | }) 143 | } 144 | 145 | func (s *State) SetSeq(_ context.Context, userID int64, seq int) error { 146 | return s.db.Update(func(tx *bolt.Tx) error { 147 | user, err := tx.CreateBucketIfNotExists(i642b(userID)) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | state := user.Bucket([]byte("state")) 153 | if state == nil { 154 | return fmt.Errorf("state not found") 155 | } 156 | return state.Put([]byte("seq"), i2b(seq)) 157 | }) 158 | } 159 | 160 | func (s *State) SetDateSeq(_ context.Context, userID int64, date, seq int) error { 161 | return s.db.Update(func(tx *bolt.Tx) error { 162 | user, err := tx.CreateBucketIfNotExists(i642b(userID)) 163 | if err != nil { 164 | return err 165 | } 166 | 167 | state := user.Bucket([]byte("state")) 168 | if state == nil { 169 | return fmt.Errorf("state not found") 170 | } 171 | if err := state.Put([]byte("date"), i2b(date)); err != nil { 172 | return err 173 | } 174 | return state.Put([]byte("seq"), i2b(seq)) 175 | }) 176 | } 177 | 178 | func (s *State) GetChannelPts(_ context.Context, userID, channelID int64) (pts int, found bool, err error) { 179 | tx, err := s.db.Begin(false) 180 | if err != nil { 181 | return 0, false, err 182 | } 183 | defer func() { _ = tx.Rollback() }() 184 | 185 | user := tx.Bucket(i642b(userID)) 186 | if user == nil { 187 | return 0, false, nil 188 | } 189 | 190 | channels := user.Bucket([]byte("channels")) 191 | if channels == nil { 192 | return 0, false, nil 193 | } 194 | 195 | p := channels.Get(i642b(channelID)) 196 | if p == nil { 197 | return 0, false, nil 198 | } 199 | 200 | return b2i(p), true, nil 201 | } 202 | 203 | func (s *State) SetChannelPts(_ context.Context, userID, channelID int64, pts int) error { 204 | return s.db.Update(func(tx *bolt.Tx) error { 205 | user, err := tx.CreateBucketIfNotExists(i642b(userID)) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | channels, err := user.CreateBucketIfNotExists([]byte("channels")) 211 | if err != nil { 212 | return err 213 | } 214 | return channels.Put(i642b(channelID), i2b(pts)) 215 | }) 216 | } 217 | 218 | func (s *State) ForEachChannels(ctx context.Context, userID int64, f func(ctx context.Context, channelID int64, pts int) error) error { 219 | return s.db.Update(func(tx *bolt.Tx) error { 220 | user, err := tx.CreateBucketIfNotExists(i642b(userID)) 221 | if err != nil { 222 | return err 223 | } 224 | 225 | channels, err := user.CreateBucketIfNotExists([]byte("channels")) 226 | if err != nil { 227 | return err 228 | } 229 | 230 | return channels.ForEach(func(k, v []byte) error { 231 | return f(ctx, b2i64(k), b2i(v)) 232 | }) 233 | }) 234 | } 235 | -------------------------------------------------------------------------------- /storage/peer.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/go-faster/errors" 10 | "github.com/go-faster/jx" 11 | 12 | "github.com/gotd/td/bin" 13 | "github.com/gotd/td/telegram/query/dialogs" 14 | "github.com/gotd/td/tg" 15 | ) 16 | 17 | // ErrPeerUnmarshalMustInvalidate means that persisted Peer is outdated and must be invalidated. 18 | var ErrPeerUnmarshalMustInvalidate = errors.New("outdated data for Peer (cache miss, must invalidate)") 19 | 20 | // LatestVersion is a latest supported version of data. 21 | const LatestVersion = 2 22 | 23 | // Peer is abstraction for persisted peer object. 24 | // 25 | // Note: unmarshal error ErrPeerUnmarshalMustInvalidate MUST be considered as cache miss 26 | // and cache entry MUST be invalidated. 27 | // 28 | // The only valid way to marshal and unmarshal Peer is to use UnmarshalJSON, MarshalJSON. 29 | type Peer struct { 30 | Version int 31 | Key dialogs.DialogKey 32 | CreatedAt time.Time 33 | User *tg.User 34 | Chat *tg.Chat 35 | Channel *tg.Channel 36 | Metadata map[string]any 37 | } 38 | 39 | func (p Peer) String() string { 40 | var b strings.Builder 41 | switch p.Key.Kind { 42 | case dialogs.Chat: 43 | b.WriteString("Chat") 44 | case dialogs.Channel: 45 | b.WriteString("Channel") 46 | case dialogs.User: 47 | b.WriteString("User") 48 | } 49 | b.WriteString("(") 50 | b.WriteString(strconv.FormatInt(p.Key.ID, 10)) 51 | b.WriteString(")") 52 | 53 | b.WriteString("[") 54 | var entities []string 55 | if p.User != nil { 56 | entities = append(entities, "User") 57 | } 58 | if p.Chat != nil { 59 | entities = append(entities, "Chat") 60 | } 61 | if p.Channel != nil { 62 | entities = append(entities, "Channel") 63 | } 64 | b.WriteString(strings.Join(entities, ", ")) 65 | b.WriteString("]") 66 | 67 | return b.String() 68 | } 69 | 70 | func decodeObject(d *jx.Decoder, v bin.Decoder) error { 71 | data, err := d.Base64() 72 | if err != nil { 73 | return errors.Wrap(err, "base64") 74 | } 75 | b := &bin.Buffer{ 76 | Buf: data, 77 | } 78 | if err := v.Decode(b); err != nil { 79 | return errors.Wrap(err, "decode") 80 | } 81 | return nil 82 | } 83 | 84 | func (p *Peer) UnmarshalJSON(data []byte) error { 85 | return p.Unmarshal(jx.DecodeBytes(data)) 86 | } 87 | 88 | func (p *Peer) Unmarshal(d *jx.Decoder) error { 89 | var version int 90 | if err := d.Capture(func(d *jx.Decoder) error { 91 | return d.Obj(func(d *jx.Decoder, key string) error { 92 | if key != "Version" { 93 | return d.Skip() 94 | } 95 | v, err := d.Int() 96 | if err != nil { 97 | return errors.Wrap(err, "version") 98 | } 99 | version = v 100 | return nil 101 | }) 102 | }); err != nil { 103 | return errors.Wrap(err, "check version") 104 | } 105 | if version != LatestVersion { 106 | // Ignoring. 107 | return ErrPeerUnmarshalMustInvalidate 108 | } 109 | 110 | // Reset. 111 | p.Metadata = nil 112 | p.User = nil 113 | p.Chat = nil 114 | p.Channel = nil 115 | p.CreatedAt = time.Time{} 116 | 117 | if err := d.Obj(func(d *jx.Decoder, key string) error { 118 | switch key { 119 | case "Version": 120 | v, err := d.Int() 121 | if err != nil { 122 | return errors.Wrap(err, "version") 123 | } 124 | p.Version = v 125 | return nil 126 | case "CreatedAt": 127 | v, err := d.Int64() 128 | if err != nil { 129 | return errors.Wrap(err, "created_at") 130 | } 131 | p.CreatedAt = time.Unix(v, 0) 132 | return nil 133 | case "Key": 134 | return d.Obj(func(d *jx.Decoder, key string) error { 135 | switch key { 136 | case "Kind": 137 | v, err := d.Int() 138 | if err != nil { 139 | return errors.Wrap(err, "kind") 140 | } 141 | p.Key.Kind = dialogs.PeerKind(v) 142 | case "ID": 143 | v, err := d.Int64() 144 | if err != nil { 145 | return errors.Wrap(err, "id") 146 | } 147 | p.Key.ID = v 148 | case "AccessHash": 149 | v, err := d.Int64() 150 | if err != nil { 151 | return errors.Wrap(err, "access_hash") 152 | } 153 | p.Key.AccessHash = v 154 | default: 155 | return d.Skip() 156 | } 157 | return nil 158 | }) 159 | case "Metadata": 160 | var metadata map[string]any 161 | buf, err := d.Raw() 162 | if err != nil { 163 | return errors.Wrap(err, "metadata") 164 | } 165 | if err := json.Unmarshal(buf, &metadata); err != nil { 166 | return errors.Wrap(err, "unmarshal metadata") 167 | } 168 | p.Metadata = metadata 169 | return nil 170 | case "User": 171 | var user tg.User 172 | if err := decodeObject(d, &user); err != nil { 173 | return errors.Wrap(err, "user") 174 | } 175 | p.User = &user 176 | return nil 177 | case "Chat": 178 | var chat tg.Chat 179 | if err := decodeObject(d, &chat); err != nil { 180 | return errors.Wrap(err, "chat") 181 | } 182 | p.Chat = &chat 183 | return nil 184 | case "Channel": 185 | var channel tg.Channel 186 | if err := decodeObject(d, &channel); err != nil { 187 | return errors.Wrap(err, "channel") 188 | } 189 | p.Channel = &channel 190 | return nil 191 | default: 192 | return d.Skip() 193 | } 194 | }); err != nil { 195 | if _, ok := errors.Into[*bin.UnexpectedIDErr](err); ok { 196 | return ErrPeerUnmarshalMustInvalidate 197 | } 198 | return errors.Wrap(err, "decode") 199 | } 200 | 201 | return nil 202 | } 203 | 204 | func (p Peer) Marshal(e *jx.Encoder) error { 205 | type rawObject struct { 206 | Key string 207 | Value bin.Encoder 208 | } 209 | var toEncode []rawObject 210 | if p.User != nil { 211 | toEncode = append(toEncode, rawObject{Key: "User", Value: p.User}) 212 | } 213 | if p.Chat != nil { 214 | toEncode = append(toEncode, rawObject{Key: "Chat", Value: p.Chat}) 215 | } 216 | if p.Channel != nil { 217 | toEncode = append(toEncode, rawObject{Key: "Channel", Value: p.Channel}) 218 | } 219 | type rawValue struct { 220 | Key string 221 | Value []byte 222 | } 223 | var values []rawValue 224 | for _, v := range toEncode { 225 | if v.Value == nil { 226 | continue 227 | } 228 | b := new(bin.Buffer) 229 | if err := v.Value.Encode(b); err != nil { 230 | return errors.Wrap(err, "encode") 231 | } 232 | values = append(values, rawValue{ 233 | Key: v.Key, 234 | Value: b.Buf, 235 | }) 236 | } 237 | 238 | metadataRaw, err := json.Marshal(p.Metadata) 239 | if err != nil { 240 | return errors.Wrap(err, "marshal metadata") 241 | } 242 | 243 | e.Obj(func(e *jx.Encoder) { 244 | e.Field("Version", func(e *jx.Encoder) { 245 | e.Int(p.Version) 246 | }) 247 | e.Field("Key", func(e *jx.Encoder) { 248 | e.Obj(func(e *jx.Encoder) { 249 | e.Field("Kind", func(e *jx.Encoder) { 250 | e.Int(int(p.Key.Kind)) 251 | }) 252 | e.Field("ID", func(e *jx.Encoder) { 253 | e.Int64(p.Key.ID) 254 | }) 255 | e.Field("AccessHash", func(e *jx.Encoder) { 256 | e.Int64(p.Key.AccessHash) 257 | }) 258 | }) 259 | }) 260 | e.Field("CreatedAt", func(e *jx.Encoder) { 261 | e.Int64(p.CreatedAt.Unix()) 262 | }) 263 | e.Field("Metadata", func(e *jx.Encoder) { 264 | e.Raw(metadataRaw) 265 | }) 266 | for _, v := range values { 267 | e.Field(v.Key, func(e *jx.Encoder) { 268 | e.Base64(v.Value) 269 | }) 270 | } 271 | }) 272 | return nil 273 | } 274 | 275 | func (p Peer) MarshalJSON() ([]byte, error) { 276 | var e jx.Encoder 277 | if err := p.Marshal(&e); err != nil { 278 | return nil, err 279 | } 280 | return e.Bytes(), nil 281 | } 282 | 283 | func addIfNotEmpty(r []string, k string) []string { 284 | if k == "" { 285 | return r 286 | } 287 | return append(r, k) 288 | } 289 | 290 | // Keys returns list of all associated keys (phones, usernames, etc.) stored in the peer. 291 | func (p *Peer) Keys() []string { 292 | // Chat does not contain usernames or phones. 293 | if p.Chat != nil { 294 | return nil 295 | } 296 | 297 | r := make([]string, 0, 4) 298 | switch { 299 | case p.User != nil: 300 | r = addIfNotEmpty(r, p.User.Username) 301 | r = addIfNotEmpty(r, p.User.Phone) 302 | case p.Channel != nil: 303 | r = addIfNotEmpty(r, p.Channel.Username) 304 | } 305 | 306 | return r 307 | } 308 | 309 | // FromInputPeer fills Peer object using given tg.InputPeerClass. 310 | func (p *Peer) FromInputPeer(input tg.InputPeerClass) error { 311 | k := dialogs.DialogKey{} 312 | if err := k.FromInputPeer(input); err != nil { 313 | return errors.Errorf("unpack input peer: %w", err) 314 | } 315 | 316 | *p = Peer{ 317 | Version: LatestVersion, 318 | Key: k, 319 | CreatedAt: time.Now(), 320 | } 321 | 322 | return nil 323 | } 324 | 325 | // FromChat fills Peer object using given tg.ChatClass. 326 | func (p *Peer) FromChat(chat tg.ChatClass) bool { 327 | r := Peer{ 328 | Version: LatestVersion, 329 | CreatedAt: time.Now(), 330 | } 331 | 332 | switch c := chat.(type) { 333 | case *tg.Chat: 334 | r.Key.ID = c.ID 335 | r.Key.Kind = dialogs.Chat 336 | r.Chat = c 337 | case *tg.ChatForbidden: 338 | r.Key.ID = c.ID 339 | r.Key.Kind = dialogs.Chat 340 | case *tg.Channel: 341 | if c.Min { 342 | return false 343 | } 344 | r.Key.ID = c.ID 345 | r.Key.AccessHash = c.AccessHash 346 | r.Key.Kind = dialogs.Channel 347 | r.Channel = c 348 | case *tg.ChannelForbidden: 349 | r.Key.ID = c.ID 350 | r.Key.AccessHash = c.AccessHash 351 | r.Key.Kind = dialogs.Channel 352 | default: 353 | return false 354 | } 355 | 356 | *p = r 357 | return true 358 | } 359 | 360 | // FromUser fills Peer object using given tg.UserClass. 361 | func (p *Peer) FromUser(user tg.UserClass) bool { 362 | u, ok := user.AsNotEmpty() 363 | if !ok { 364 | return false 365 | } 366 | 367 | *p = Peer{ 368 | Version: LatestVersion, 369 | CreatedAt: time.Now(), 370 | User: u, 371 | Key: dialogs.DialogKey{ 372 | Kind: dialogs.User, 373 | ID: u.ID, 374 | AccessHash: u.AccessHash, 375 | }, 376 | } 377 | 378 | return true 379 | } 380 | --------------------------------------------------------------------------------