You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

173 lines
4.2 KiB

package telegram
import (
"context"
"io"
"os"
"path/filepath"
"strconv"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/go-faster/errors"
"go.uber.org/zap"
"golang.org/x/net/proxy"
"github.com/gotd/td/clock"
"github.com/gotd/td/internal/crypto"
"github.com/gotd/td/session"
"github.com/gotd/td/telegram/auth"
"github.com/gotd/td/telegram/dcs"
"github.com/gotd/td/tgerr"
)
func sessionDir() (string, error) {
dir, ok := os.LookupEnv("SESSION_DIR")
if ok {
return filepath.Abs(dir)
}
dir, err := os.UserHomeDir()
if err != nil {
dir = "."
}
return filepath.Abs(filepath.Join(dir, ".td"))
}
// OptionsFromEnvironment fills unfilled field in opts parameter
// using environment variables.
//
// Variables:
// SESSION_FILE: path to session file
// SESSION_DIR: path to session directory, if SESSION_FILE is not set
// ALL_PROXY, NO_PROXY: see https://pkg.go.dev/golang.org/x/net/proxy#FromEnvironment
func OptionsFromEnvironment(opts Options) (Options, error) {
// Setting up session storage if not provided.
if opts.SessionStorage == nil {
sessionFile, ok := os.LookupEnv("SESSION_FILE")
if !ok {
dir, err := sessionDir()
if err != nil {
return Options{}, errors.Wrap(err, "SESSION_DIR not set or invalid")
}
sessionFile = filepath.Join(dir, "session.json")
}
dir, _ := filepath.Split(sessionFile)
if err := os.MkdirAll(dir, 0700); err != nil {
return Options{}, errors.Wrap(err, "session dir creation")
}
opts.SessionStorage = &session.FileStorage{
Path: sessionFile,
}
}
if opts.Resolver == nil {
opts.Resolver = dcs.Plain(dcs.PlainOptions{
Dial: proxy.Dial,
})
}
return opts, nil
}
// ClientFromEnvironment creates client using OptionsFromEnvironment
// but does not connect to server.
//
// Variables:
// APP_ID: app_id of Telegram app.
// APP_HASH: app_hash of Telegram app.
func ClientFromEnvironment(opts Options) (*Client, error) {
appID, err := strconv.Atoi(os.Getenv("APP_ID"))
if err != nil {
return nil, errors.Wrap(err, "APP_ID not set or invalid")
}
appHash := os.Getenv("APP_HASH")
if appHash == "" {
return nil, errors.New("no APP_HASH provided")
}
opts, err = OptionsFromEnvironment(opts)
if err != nil {
return nil, err
}
return NewClient(appID, appHash, opts), nil
}
func retry(ctx context.Context, logger *zap.Logger, cb func(ctx context.Context) error) error {
b := backoff.WithContext(backoff.NewExponentialBackOff(), ctx)
// List of known retryable RPC error types.
retryableErrors := []string{
"NEED_MEMBER_INVALID",
"AUTH_KEY_UNREGISTERED",
"API_ID_PUBLISHED_FLOOD",
}
return backoff.Retry(func() error {
if err := cb(ctx); err != nil {
logger.Warn("TestClient run failed", zap.Error(err))
if tgerr.Is(err, retryableErrors...) {
return err
}
if timeout, ok := AsFloodWait(err); ok {
timer := clock.System.Timer(timeout + 1*time.Second)
defer clock.StopTimer(timer)
select {
case <-timer.C():
return err
case <-ctx.Done():
return ctx.Err()
}
}
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
// Possibly server closed connection.
return err
}
return backoff.Permanent(err)
}
return nil
}, b)
}
// TestClient creates and authenticates user telegram.Client
// using Telegram test server.
func TestClient(ctx context.Context, opts Options, cb func(ctx context.Context, client *Client) error) error {
if opts.DC == 0 {
opts.DC = 2
}
if opts.DCList.Zero() {
opts.DCList = dcs.Test()
}
logger := zap.NewNop()
if opts.Logger != nil {
logger = opts.Logger.Named("test")
}
// Sometimes testing server can return "AUTH_KEY_UNREGISTERED" error.
// It is expected and client implementation is unlikely to cause
// such errors, so just doing retries using backoff.
return retry(ctx, logger, func(retryCtx context.Context) error {
client := NewClient(TestAppID, TestAppHash, opts)
return client.Run(retryCtx, func(runCtx context.Context) error {
if err := client.Auth().IfNecessary(runCtx, auth.NewFlow(
auth.Test(crypto.DefaultRand(), opts.DC),
auth.SendCodeOptions{},
)); err != nil {
return errors.Wrap(err, "auth flow")
}
return cb(runCtx, client)
})
})
}