├── .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 | [](https://github.com/gotd/contrib/actions)
4 | [](https://pkg.go.dev/github.com/gotd/contrib)
5 | [](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 |
--------------------------------------------------------------------------------