> 📥 Telegram Downloader, but more than a downloader
English | 简体中文
> 📥 Telegram Downloader, but more than a downloader
English | 简体中文
"+FileName+" - "+MIME+""`, "caption for the uploaded media")
// completion and validation
_ = cmd.MarkFlagRequired(path)
cmd.MarkFlagsMutuallyExclusive(include, exclude)
return cmd
}
================================================
FILE: cmd/version.go
================================================
package cmd
import (
"bytes"
_ "embed"
"runtime"
"text/template"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/iyear/tdl/pkg/consts"
)
//go:embed version.tmpl
var version string
func NewVersion() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Check the version info",
RunE: func(cmd *cobra.Command, args []string) error {
buf := &bytes.Buffer{}
if err := template.Must(template.New("version").Parse(version)).Execute(buf, map[string]interface{}{
"Version": consts.Version,
"Commit": consts.Commit,
"Date": consts.CommitDate,
"GoVersion": runtime.Version(),
"GOOS": runtime.GOOS,
"GOARCH": runtime.GOARCH,
}); err != nil {
return err
}
color.Blue(buf.String())
return nil
},
}
}
================================================
FILE: cmd/version.tmpl
================================================
Version: {{ .Version }}
Commit: {{ .Commit }}
Date: {{ .Date }}
{{ .GoVersion }} {{ .GOOS }}/{{ .GOARCH }}
================================================
FILE: core/dcpool/dcpool.go
================================================
package dcpool
import (
"context"
"sync"
"github.com/gotd/td/telegram"
"github.com/gotd/td/tg"
"go.uber.org/multierr"
"go.uber.org/zap"
"github.com/iyear/tdl/core/logctx"
"github.com/iyear/tdl/core/middlewares/takeout"
)
var testMode = false
// EnableTestMode enables test mode, which disables takeout and pooling and directly returns original client.
func EnableTestMode() {
testMode = true
}
type Pool interface {
Client(ctx context.Context, dc int) *tg.Client
Takeout(ctx context.Context, dc int) *tg.Client
Default(ctx context.Context) *tg.Client
Close() error
}
type pool struct {
api *telegram.Client
size int64
mu *sync.Mutex
middlewares []telegram.Middleware
invokers map[int]tg.Invoker
closes map[int]func() error
takeout int64
}
func NewPool(c *telegram.Client, size int64, middlewares ...telegram.Middleware) Pool {
return &pool{
api: c,
size: size,
mu: &sync.Mutex{},
middlewares: middlewares,
invokers: make(map[int]tg.Invoker),
closes: make(map[int]func() error),
takeout: 0,
}
}
func (p *pool) current() int {
return p.api.Config().ThisDC
}
func (p *pool) Client(ctx context.Context, dc int) *tg.Client {
p.mu.Lock()
defer p.mu.Unlock()
return tg.NewClient(p.invoker(ctx, dc))
}
func (p *pool) invoker(ctx context.Context, dc int) tg.Invoker {
// self-hosted Telegram server can't properly handle pooling connections,
// so directly return original client
if testMode {
return p.api
}
if i, ok := p.invokers[dc]; ok {
return i
}
// lazy init
var (
invoker telegram.CloseInvoker
err error
)
if dc == p.current() { // can't transfer dc to current dc
invoker, err = p.api.Pool(p.size)
} else {
invoker, err = p.api.DC(ctx, dc, p.size)
}
if err != nil {
logctx.From(ctx).Error("create invoker", zap.Error(err))
return p.api // degraded
}
p.closes[dc] = invoker.Close
p.invokers[dc] = chainMiddlewares(invoker, p.middlewares...)
return p.invokers[dc]
}
func (p *pool) Default(ctx context.Context) *tg.Client {
return p.Client(ctx, p.current())
}
func (p *pool) Close() (err error) {
if p.takeout != 0 {
err = takeout.UnTakeout(context.TODO(), p.Takeout(context.TODO(), p.current()).Invoker())
}
for _, c := range p.closes {
err = multierr.Append(err, c())
}
return err
}
func (p *pool) Takeout(ctx context.Context, dc int) *tg.Client {
p.mu.Lock()
defer p.mu.Unlock()
// lazy init
if p.takeout == 0 {
sid, err := takeout.Takeout(ctx, p.api)
if err != nil {
logctx.From(ctx).Warn("takeout error", zap.Error(err))
// ignore init delay error and return non-takeout client
return p.Client(ctx, dc)
}
p.takeout = sid
logctx.From(ctx).Info("get takeout id", zap.Int64("id", sid))
}
return tg.NewClient(chainMiddlewares(p.invoker(ctx, dc), takeout.Middleware(p.takeout)))
}
================================================
FILE: core/dcpool/middlewares.go
================================================
package dcpool
import (
"github.com/gotd/td/telegram"
"github.com/gotd/td/tg"
)
func chainMiddlewares(invoker tg.Invoker, chain ...telegram.Middleware) tg.Invoker {
if len(chain) == 0 {
return invoker
}
for i := len(chain) - 1; i >= 0; i-- {
invoker = chain[i].Handle(invoker)
}
return invoker
}
================================================
FILE: core/downloader/downloader.go
================================================
package downloader
import (
"context"
"github.com/go-faster/errors"
"github.com/gotd/td/telegram/downloader"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
"github.com/iyear/tdl/core/dcpool"
"github.com/iyear/tdl/core/logctx"
"github.com/iyear/tdl/core/util/tutil"
)
// MaxPartSize refer to https://core.telegram.org/api/files#downloading-files
const MaxPartSize = 1024 * 1024
type Downloader struct {
opts Options
}
type Options struct {
Pool dcpool.Pool
Threads int
Iter Iter
Progress Progress
}
func New(opts Options) *Downloader {
return &Downloader{
opts: opts,
}
}
func (d *Downloader) Download(ctx context.Context, limit int) error {
wg, wgctx := errgroup.WithContext(ctx)
wg.SetLimit(limit)
for d.opts.Iter.Next(wgctx) {
elem := d.opts.Iter.Value()
wg.Go(func() (rerr error) {
d.opts.Progress.OnAdd(elem)
defer func() { d.opts.Progress.OnDone(elem, rerr) }()
if err := d.download(wgctx, elem); err != nil {
// canceled by user, so we directly return error to stop all
if errors.Is(err, context.Canceled) {
return errors.Wrap(err, "download")
}
// don't return error, just log it
logctx.
From(ctx).
Error("Download error",
zap.Any("element", elem),
zap.Error(err),
)
}
return nil
})
}
if err := d.opts.Iter.Err(); err != nil {
return errors.Wrap(err, "iter")
}
return wg.Wait()
}
func (d *Downloader) download(ctx context.Context, elem Elem) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
logctx.From(ctx).Debug("Start download elem",
zap.Any("elem", elem))
client := d.opts.Pool.Client(ctx, elem.File().DC())
if elem.AsTakeout() {
client = d.opts.Pool.Takeout(ctx, elem.File().DC())
}
_, err := downloader.NewDownloader().WithPartSize(MaxPartSize).
Download(client, elem.File().Location()).
WithThreads(tutil.BestThreads(elem.File().Size(), d.opts.Threads)).
Parallel(ctx, newWriteAt(elem, d.opts.Progress, MaxPartSize))
if err != nil {
return errors.Wrap(err, "download")
}
return nil
}
================================================
FILE: core/downloader/iter.go
================================================
package downloader
import (
"context"
"io"
"github.com/gotd/td/tg"
)
type Iter interface {
Next(ctx context.Context) bool
Value() Elem
Err() error
}
type Elem interface {
File() File
To() io.WriterAt
AsTakeout() bool
}
type File interface {
Location() tg.InputFileLocationClass
Size() int64
DC() int
}
================================================
FILE: core/downloader/progress.go
================================================
package downloader
import (
"time"
"go.uber.org/atomic"
)
type Progress interface {
OnAdd(elem Elem)
OnDownload(elem Elem, state ProgressState)
OnDone(elem Elem, err error)
// TODO: OnLog to log something that is not an error but should be sent to the user
}
type ProgressState struct {
Downloaded int64
Total int64
}
// writeAt wrapper for file to use progress bar
//
// do not need mutex because gotd has use syncio.WriteAt
type writeAt struct {
elem Elem
progress Progress
partSize int
downloaded *atomic.Int64
}
func newWriteAt(elem Elem, progress Progress, partSize int) *writeAt {
return &writeAt{
elem: elem,
progress: progress,
partSize: partSize,
downloaded: atomic.NewInt64(0),
}
}
func (w *writeAt) WriteAt(p []byte, off int64) (int, error) {
at, err := w.elem.To().WriteAt(p, off)
if err != nil {
return 0, err
}
// some small files may finish too fast, terminal history may not be overwritten
// this is just a simple way to avoid the problem
if at < w.partSize { // last part(every file only exec once)
time.Sleep(time.Millisecond * 200) // to ensure the progress render next time
}
w.progress.OnDownload(w.elem, ProgressState{
Downloaded: w.downloaded.Add(int64(at)),
Total: w.elem.File().Size(),
})
return at, nil
}
================================================
FILE: core/forwarder/clone.go
================================================
package forwarder
import (
"context"
"io"
"os"
"github.com/go-faster/errors"
"github.com/gotd/td/telegram/downloader"
"github.com/gotd/td/telegram/uploader"
"github.com/gotd/td/tg"
"go.uber.org/atomic"
"go.uber.org/multierr"
tdownloader "github.com/iyear/tdl/core/downloader"
"github.com/iyear/tdl/core/tmedia"
tuploader "github.com/iyear/tdl/core/uploader"
"github.com/iyear/tdl/core/util/tutil"
)
type cloneOptions struct {
elem Elem
media *tmedia.Media
progress progressAdd
}
type progressAdd interface {
add(n int64)
}
func (f *Forwarder) cloneMedia(ctx context.Context, opts cloneOptions, dryRun bool) (_ tg.InputFileClass, rerr error) {
// if dry run, just return empty input file
if dryRun {
// directly call progress callback
opts.progress.add(opts.media.Size * 2)
return &tg.InputFile{}, nil
}
temp, err := os.CreateTemp("", "tdl_*")
if err != nil {
return nil, errors.Wrap(err, "create temp file")
}
defer func() {
multierr.AppendInto(&rerr, temp.Close())
multierr.AppendInto(&rerr, os.Remove(temp.Name()))
}()
threads := tutil.BestThreads(opts.media.Size, f.opts.Threads)
_, err = downloader.NewDownloader().
WithPartSize(tdownloader.MaxPartSize).
Download(f.opts.Pool.Client(ctx, opts.media.DC), opts.media.InputFileLoc).
WithThreads(threads).
Parallel(ctx, writeAt{
f: temp,
opts: opts,
})
if err != nil {
return nil, errors.Wrap(err, "download")
}
var file tg.InputFileClass
if _, err = temp.Seek(0, io.SeekStart); err != nil {
return nil, errors.Wrap(err, "seek")
}
upload := uploader.NewUpload(opts.media.Name, temp, opts.media.Size)
file, err = uploader.NewUploader(f.opts.Pool.Default(ctx)).
WithPartSize(tuploader.MaxPartSize).
WithThreads(threads).
WithProgress(uploaded{
opts: opts,
prev: atomic.NewInt64(0),
}).
Upload(ctx, upload)
if err != nil {
return nil, errors.Wrap(err, "upload")
}
return file, nil
}
type writeAt struct {
f io.WriterAt
opts cloneOptions
}
func (w writeAt) WriteAt(p []byte, off int64) (int, error) {
n, err := w.f.WriteAt(p, off)
if err != nil {
return 0, err
}
w.opts.progress.add(int64(n))
return n, nil
}
type uploaded struct {
opts cloneOptions
prev *atomic.Int64
}
func (u uploaded) Chunk(_ context.Context, state uploader.ProgressState) error {
u.opts.progress.add(state.Uploaded - u.prev.Swap(state.Uploaded))
return nil
}
================================================
FILE: core/forwarder/forwarder.go
================================================
package forwarder
import (
"context"
"math/rand"
"time"
"github.com/go-faster/errors"
"github.com/gotd/td/bin"
"github.com/gotd/td/telegram/peers"
"github.com/gotd/td/tg"
"go.uber.org/atomic"
"go.uber.org/zap"
"github.com/iyear/tdl/core/dcpool"
"github.com/iyear/tdl/core/logctx"
"github.com/iyear/tdl/core/tmedia"
"github.com/iyear/tdl/core/util/tutil"
)
//go:generate go-enum --values --names --flag --nocase
// Mode
// ENUM(direct, clone)
type Mode int
type Options struct {
Pool dcpool.Pool
Threads int
Iter Iter
Progress Progress
}
type Forwarder struct {
sent map[tuple]struct{} // used to filter grouped messages which are already sent
rand *rand.Rand
opts Options
}
type tuple struct {
from int64
msg int
}
func New(opts Options) *Forwarder {
return &Forwarder{
sent: make(map[tuple]struct{}),
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
opts: opts,
}
}
func (f *Forwarder) Forward(ctx context.Context) error {
for f.opts.Iter.Next(ctx) {
elem := f.opts.Iter.Value()
if _, ok := f.sent[f.tuple(elem.From(), elem.Msg())]; ok {
// skip grouped messages
continue
}
if _, ok := elem.Msg().GetGroupedID(); ok && elem.AsGrouped() {
grouped, err := tutil.GetGroupedMessages(ctx, f.opts.Pool.Default(ctx), elem.From().InputPeer(), elem.Msg())
if err != nil {
continue
}
if err = f.forwardMessage(ctx, elem, grouped...); err != nil {
continue
}
continue
}
if err := f.forwardMessage(ctx, elem); err != nil {
// canceled by user, so we directly return error to stop all
if errors.Is(err, context.Canceled) {
return err
}
continue
}
}
return f.opts.Iter.Err()
}
func (f *Forwarder) forwardMessage(ctx context.Context, elem Elem, grouped ...*tg.Message) (rerr error) {
f.opts.Progress.OnAdd(elem)
defer func() {
f.sent[f.tuple(elem.From(), elem.Msg())] = struct{}{}
// grouped message also should be marked as sent
for _, m := range grouped {
f.sent[f.tuple(elem.From(), m)] = struct{}{}
}
f.opts.Progress.OnDone(elem, rerr)
}()
log := logctx.From(ctx).With(
zap.Int64("from", elem.From().ID()),
zap.Int64("to", elem.To().ID()),
zap.Int("message", elem.Msg().ID))
// used for clone progress
totalSize, err := mediaSizeSum(elem.Msg(), grouped...)
if err != nil {
return errors.Wrap(err, "media total size")
}
done := atomic.NewInt64(0)
forwardTextOnly := func(msg *tg.Message) error {
if msg.Message == "" {
return errors.Errorf("empty message content, skip send: %d", msg.ID)
}
req := &tg.MessagesSendMessageRequest{
NoWebpage: false,
Silent: elem.AsSilent(),
Background: false,
ClearDraft: false,
Noforwards: false,
UpdateStickersetsOrder: false,
Peer: elem.To().InputPeer(),
ReplyTo: getReplyTo(elem.Thread()),
Message: msg.Message,
RandomID: f.rand.Int63(),
ReplyMarkup: msg.ReplyMarkup,
Entities: msg.Entities,
ScheduleDate: 0,
SendAs: nil,
}
req.SetFlags()
if _, err := f.forwardClient(ctx, elem).MessagesSendMessage(ctx, req); err != nil {
return errors.Wrap(err, "send message")
}
return nil
}
convForwardedMedia := func(msg *tg.Message) (tg.InputMediaClass, error) {
if _, hasMedia := msg.GetMedia(); !hasMedia {
// media can't be forwarded via simple copy(it depends on the server ids)
// if it's not a media message, just break and send text copy
return nil, errors.Errorf("message %d is not a media message", msg.ID)
}
// if it's a media message, but it's not protected, convert it to InputMediaClass
// or if it's protected, but it doesn't contain photo or document,
// we should clone photo and document via re-upload, it will be banned if we forward it directly.
// but other media can be forwarded directly via copy
if (!protectedDialog(elem.From()) && !protectedMessage(msg)) || !photoOrDocument(msg.Media) {
media, ok := tmedia.ConvInputMedia(msg.Media)
if !ok {
return nil, errors.Errorf("can't convert message %d to input class directly", msg.ID)
}
return media, nil
}
media, ok := tmedia.GetMedia(msg)
if !ok {
log.Warn("Can't get media from message",
zap.Int64("peer", elem.From().ID()),
zap.Int("message", msg.ID))
// unsupported re-upload media
return nil, errors.Errorf("unsupported media %T", msg.Media)
}
mediaFile, err := f.cloneMedia(ctx, cloneOptions{
elem: elem,
media: media,
progress: &wrapProgress{
elem: elem,
progress: f.opts.Progress,
done: done,
total: totalSize * 2,
},
}, elem.AsDryRun())
if err != nil {
return nil, errors.Wrap(err, "clone media")
}
var inputMedia tg.InputMediaClass
// now we only have to process cloned photo or document
switch m := msg.Media.(type) {
case *tg.MessageMediaPhoto:
photo := &tg.InputMediaUploadedPhoto{
Spoiler: m.Spoiler,
File: mediaFile,
TTLSeconds: m.TTLSeconds,
}
photo.SetFlags()
inputMedia = photo
case *tg.MessageMediaDocument:
doc, ok := m.Document.AsNotEmpty()
if !ok {
return nil, errors.Errorf("empty document %d", msg.ID)
}
document := &tg.InputMediaUploadedDocument{
NosoundVideo: false, // do not set
ForceFile: false, // do not set
Spoiler: m.Spoiler,
File: mediaFile,
MimeType: doc.MimeType,
Attributes: doc.Attributes,
Stickers: nil, // do not set
TTLSeconds: 0, // do not set
}
if thumb, ok := tmedia.GetDocumentThumb(doc); ok {
thumbFile, err := f.cloneMedia(ctx, cloneOptions{
elem: elem,
media: thumb,
progress: nopProgress{},
}, elem.AsDryRun())
if err != nil {
return nil, errors.Wrap(err, "clone thumb")
}
document.Thumb = thumbFile
}
document.SetFlags()
inputMedia = document
default:
return nil, errors.Errorf("unsupported media %T", msg.Media)
}
// note that they must be separately uploaded using messages uploadMedia first,
// using raw inputMediaUploaded* constructors is not supported.
messageMedia, err := f.forwardClient(ctx, elem).MessagesUploadMedia(ctx, &tg.MessagesUploadMediaRequest{
Peer: elem.To().InputPeer(),
Media: inputMedia,
})
if err != nil {
return nil, errors.Wrap(err, "upload media")
}
inputMedia, ok = tmedia.ConvInputMedia(messageMedia)
if !ok && !elem.AsDryRun() {
return nil, errors.Errorf("can't convert uploaded media to input class")
}
return inputMedia, nil
}
switch elem.Mode() {
case ModeDirect:
// it can be forwarded via API
if !protectedDialog(elem.From()) && !protectedMessage(elem.Msg()) {
directForward := func(ids ...int) error {
randIDs := make([]int64, 0, len(ids))
for range ids {
randIDs = append(randIDs, f.rand.Int63())
}
req := &tg.MessagesForwardMessagesRequest{
Silent: elem.AsSilent(),
Background: false,
WithMyScore: false,
DropAuthor: false,
DropMediaCaptions: false,
Noforwards: false,
FromPeer: elem.From().InputPeer(),
ID: ids,
RandomID: randIDs,
ToPeer: elem.To().InputPeer(),
TopMsgID: elem.Thread(),
ScheduleDate: 0,
SendAs: nil,
}
req.SetFlags()
if _, err := f.forwardClient(ctx, elem).MessagesForwardMessages(ctx, req); err != nil {
return errors.Wrap(err, "directly forward")
}
return nil
}
if len(grouped) > 0 {
ids := make([]int, 0, len(grouped))
for _, m := range grouped {
ids = append(ids, m.ID)
}
if err = directForward(ids...); err != nil {
goto fallback
}
return nil
}
if err = directForward(elem.Msg().ID); err != nil {
goto fallback
}
return nil
}
fallback:
fallthrough
case ModeClone:
if len(grouped) > 0 {
media := make([]tg.InputSingleMedia, 0, len(grouped))
for _, gm := range grouped {
m, err := convForwardedMedia(gm)
if err != nil {
log.Debug("Can't convert forwarded media", zap.Error(err))
continue
}
single := tg.InputSingleMedia{
Media: m,
RandomID: f.rand.Int63(),
Message: gm.Message,
Entities: gm.Entities,
}
single.SetFlags()
media = append(media, single)
}
if len(media) > 0 {
req := &tg.MessagesSendMultiMediaRequest{
Silent: elem.AsSilent(),
Background: false,
ClearDraft: false,
Noforwards: false,
UpdateStickersetsOrder: false,
Peer: elem.To().InputPeer(),
ReplyTo: getReplyTo(elem.Thread()),
MultiMedia: media,
ScheduleDate: 0,
SendAs: nil,
}
req.SetFlags()
if _, err := f.forwardClient(ctx, elem).MessagesSendMultiMedia(ctx, req); err != nil {
return errors.Wrap(err, "send multi media")
}
return nil
}
return forwardTextOnly(elem.Msg())
}
media, err := convForwardedMedia(elem.Msg())
if err != nil {
log.Debug("Can't convert forwarded media", zap.Error(err))
return forwardTextOnly(elem.Msg())
}
// send text copy with forwarded media
req := &tg.MessagesSendMediaRequest{
Silent: elem.AsSilent(),
Background: false,
ClearDraft: false,
Noforwards: false,
UpdateStickersetsOrder: false,
Peer: elem.To().InputPeer(),
ReplyTo: getReplyTo(elem.Thread()),
Media: media,
Message: elem.Msg().Message,
RandomID: rand.Int63(),
ReplyMarkup: elem.Msg().ReplyMarkup,
Entities: elem.Msg().Entities,
ScheduleDate: 0,
SendAs: nil,
}
req.SetFlags()
if _, err := f.forwardClient(ctx, elem).MessagesSendMedia(ctx, req); err != nil {
return errors.Wrap(err, "send single media")
}
return nil
}
return errors.Errorf("unsupported mode %v", elem.Mode())
}
func (f *Forwarder) tuple(peer peers.Peer, msg *tg.Message) tuple {
return tuple{
from: peer.ID(),
msg: msg.ID,
}
}
type nopInvoker struct{}
func (n nopInvoker) Invoke(_ context.Context, _ bin.Encoder, _ bin.Decoder) error {
return nil
}
type nopProgress struct{}
func (nopProgress) add(_ int64) {}
type wrapProgress struct {
elem Elem
progress ProgressClone
done *atomic.Int64
total int64
}
func (w *wrapProgress) add(n int64) {
w.progress.OnClone(w.elem, ProgressState{
Done: w.done.Add(n),
Total: w.total,
})
}
func (f *Forwarder) forwardClient(ctx context.Context, elem Elem) *tg.Client {
if elem.AsDryRun() {
return tg.NewClient(nopInvoker{})
}
return f.opts.Pool.Default(ctx)
}
func protectedDialog(peer peers.Peer) bool {
switch p := peer.(type) {
case peers.Chat:
return p.Raw().GetNoforwards()
case peers.Channel:
return p.Raw().GetNoforwards()
}
return false
}
func protectedMessage(msg *tg.Message) bool {
return msg.GetNoforwards()
}
func photoOrDocument(media tg.MessageMediaClass) bool {
switch media.(type) {
case *tg.MessageMediaPhoto, *tg.MessageMediaDocument:
return true
default:
return false
}
}
func mediaSizeSum(msg *tg.Message, grouped ...*tg.Message) (int64, error) {
if len(grouped) > 0 {
total := int64(0)
for _, gm := range grouped {
m, ok := tmedia.GetMedia(gm)
if !ok {
return 0, errors.Errorf("can't get media from message %d", gm.ID)
}
total += m.Size
}
return total, nil
}
m, ok := tmedia.GetMedia(msg)
if !ok { // maybe it's a text only message
return 0, nil
}
return m.Size, nil
}
func getReplyTo(thread int) tg.InputReplyToClass {
replyTo := &tg.InputReplyToMessage{
ReplyToMsgID: thread,
}
replyTo.SetFlags()
return replyTo
}
================================================
FILE: core/forwarder/forwarder_enum.go
================================================
// Code generated by go-enum DO NOT EDIT.
// Version: 0.5.8
// Revision: 3d844c8ecc59661ed7aa17bfd65727bc06a60ad8
// Build Date: 2023-09-18T14:55:21Z
// Built By: goreleaser
package forwarder
import (
"fmt"
"strings"
)
const (
// ModeDirect is a Mode of type Direct.
ModeDirect Mode = iota
// ModeClone is a Mode of type Clone.
ModeClone
)
var ErrInvalidMode = fmt.Errorf("not a valid Mode, try [%s]", strings.Join(_ModeNames, ", "))
const _ModeName = "directclone"
var _ModeNames = []string{
_ModeName[0:6],
_ModeName[6:11],
}
// ModeNames returns a list of possible string values of Mode.
func ModeNames() []string {
tmp := make([]string, len(_ModeNames))
copy(tmp, _ModeNames)
return tmp
}
// ModeValues returns a list of the values for Mode
func ModeValues() []Mode {
return []Mode{
ModeDirect,
ModeClone,
}
}
var _ModeMap = map[Mode]string{
ModeDirect: _ModeName[0:6],
ModeClone: _ModeName[6:11],
}
// String implements the Stringer interface.
func (x Mode) String() string {
if str, ok := _ModeMap[x]; ok {
return str
}
return fmt.Sprintf("Mode(%d)", x)
}
// IsValid provides a quick way to determine if the typed value is
// part of the allowed enumerated values
func (x Mode) IsValid() bool {
_, ok := _ModeMap[x]
return ok
}
var _ModeValue = map[string]Mode{
_ModeName[0:6]: ModeDirect,
strings.ToLower(_ModeName[0:6]): ModeDirect,
_ModeName[6:11]: ModeClone,
strings.ToLower(_ModeName[6:11]): ModeClone,
}
// ParseMode attempts to convert a string to a Mode.
func ParseMode(name string) (Mode, error) {
if x, ok := _ModeValue[name]; ok {
return x, nil
}
// Case insensitive parse, do a separate lookup to prevent unnecessary cost of lowercasing a string if we don't need to.
if x, ok := _ModeValue[strings.ToLower(name)]; ok {
return x, nil
}
return Mode(0), fmt.Errorf("%s is %w", name, ErrInvalidMode)
}
// Set implements the Golang flag.Value interface func.
func (x *Mode) Set(val string) error {
v, err := ParseMode(val)
*x = v
return err
}
// Get implements the Golang flag.Getter interface func.
func (x *Mode) Get() interface{} {
return *x
}
// Type implements the github.com/spf13/pFlag Value interface.
func (x *Mode) Type() string {
return "Mode"
}
================================================
FILE: core/forwarder/iter.go
================================================
package forwarder
import (
"context"
"github.com/gotd/td/telegram/peers"
"github.com/gotd/td/tg"
)
type Iter interface {
Next(ctx context.Context) bool
Value() Elem
Err() error
}
type Elem interface {
Mode() Mode
From() peers.Peer
Msg() *tg.Message
To() peers.Peer
Thread() int // reply to message/topic
AsSilent() bool
AsDryRun() bool
AsGrouped() bool // detect and forward grouped messages
}
================================================
FILE: core/forwarder/progress.go
================================================
package forwarder
type ProgressClone interface {
OnClone(elem Elem, state ProgressState)
}
type Progress interface {
OnAdd(elem Elem)
ProgressClone
OnDone(elem Elem, err error)
}
type ProgressState struct {
Done int64
Total int64
}
================================================
FILE: core/go.mod
================================================
module github.com/iyear/tdl/core
go 1.25.8
require (
github.com/cenkalti/backoff/v4 v4.3.0
github.com/gabriel-vasile/mimetype v1.4.13
github.com/go-faster/errors v0.7.1
github.com/gotd/contrib v0.20.0
github.com/gotd/td v0.122.0
github.com/iyear/connectproxy v0.1.1
github.com/samber/lo v1.53.0
github.com/yapingcat/gomedia v0.0.0-20240601043430-920523f8e5c7
go.uber.org/atomic v1.11.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.1
golang.org/x/net v0.51.0
golang.org/x/sync v0.19.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
github.com/beevik/ntp v1.3.1 // indirect
github.com/coder/websocket v1.8.13 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-faster/jx v1.1.0 // indirect
github.com/go-faster/xor v1.0.0 // indirect
github.com/go-faster/yaml v0.4.6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gotd/ige v0.2.2 // indirect
github.com/gotd/neo v0.1.5 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ogen-go/ogen v1.10.1 // indirect
github.com/segmentio/asm v1.2.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.41.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
rsc.io/qr v0.2.0 // indirect
)
================================================
FILE: core/go.sum
================================================
github.com/beevik/ntp v1.3.1 h1:Y/srlT8L1yQr58kyPWFPZIxRL8ttx2SRIpVYJqZIlAM=
github.com/beevik/ntp v1.3.1/go.mod h1:fT6PylBq86Tsq23ZMEe47b7QQrZfYBFPnpzt0a9kJxw=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg=
github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg=
github.com/go-faster/xor v0.3.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
github.com/go-faster/xor v1.0.0 h1:2o8vTOgErSGHP3/7XwA5ib1FTtUsNtwCoLLBjl31X38=
github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gotd/contrib v0.20.0 h1:1Wc4+HMQiIKYQuGHVwVksIx152HFTP6B5n88dDe0ZYw=
github.com/gotd/contrib v0.20.0/go.mod h1:P6o8W4niqhDPHLA0U+SA/L7l3BQHYLULpeHfRSePn9o=
github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk=
github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0=
github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
github.com/gotd/td v0.122.0 h1:xIqoYI02ElZjj+KxOfvoUjA63m7MGWZkemM4m42aqRE=
github.com/gotd/td v0.122.0/go.mod h1:vPC2X2rcRQYAGVr9EgmQgswHcj8Ps0Tt66XylR3CxrI=
github.com/iyear/connectproxy v0.1.1 h1:JZOF/62vvwRGBWcgSyWRb0BpKD4FSs0++B5/y5pNE4c=
github.com/iyear/connectproxy v0.1.1/go.mod h1:yD4zOmSMQCmwHIT4fk8mg4k2M15z8VoMSoeY6NNJdsA=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ogen-go/ogen v1.10.1 h1:oeSN8AF9mhTVfapbMuL8pQTF2ToqyW9xXaStmOhHKTA=
github.com/ogen-go/ogen v1.10.1/go.mod h1:fXCg9PsNYEzJ8ABdmZ2A7j4hMi9EDHP53jzsNtIM3d0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yapingcat/gomedia v0.0.0-20240601043430-920523f8e5c7 h1:e9n2WNcfvs20aLgpDhKoaJgrU/EeAvuNnWLBm31Q5Fw=
github.com/yapingcat/gomedia v0.0.0-20240601043430-920523f8e5c7/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
================================================
FILE: core/logctx/logctx.go
================================================
package logctx
import (
"context"
"go.uber.org/zap"
)
type ctxKey struct{}
func From(ctx context.Context) *zap.Logger {
if l, ok := ctx.Value(ctxKey{}).(*zap.Logger); ok {
return l
}
return zap.NewNop()
}
func With(ctx context.Context, logger *zap.Logger) context.Context {
return context.WithValue(ctx, ctxKey{}, logger)
}
func Named(ctx context.Context, name string) context.Context {
return With(ctx, From(ctx).Named(name))
}
================================================
FILE: core/middlewares/recovery/recovery.go
================================================
package recovery
import (
"context"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/go-faster/errors"
"github.com/gotd/td/bin"
"github.com/gotd/td/telegram"
"github.com/gotd/td/tg"
"github.com/gotd/td/tgerr"
"go.uber.org/zap"
"github.com/iyear/tdl/core/logctx"
)
type recovery struct {
ctx context.Context
backoff backoff.BackOff
}
func New(ctx context.Context, backoff backoff.BackOff) telegram.Middleware {
return &recovery{
ctx: ctx,
backoff: backoff,
}
}
func (r *recovery) Handle(next tg.Invoker) telegram.InvokeFunc {
return func(ctx context.Context, input bin.Encoder, output bin.Decoder) error {
log := logctx.From(r.ctx)
return backoff.RetryNotify(func() error {
if err := next.Invoke(ctx, input, output); err != nil {
if r.shouldRecover(ctx, err) {
return errors.Wrap(err, "recover")
}
return backoff.Permanent(err)
}
return nil
}, r.backoff, func(err error, duration time.Duration) {
log.Debug("Wait for connection recovery", zap.Error(err), zap.Duration("duration", duration))
})
}
}
func (r *recovery) shouldRecover(ctx context.Context, err error) bool {
// context in recovery is used to stop recovery process by external os signal, otherwise we will wait till max retries when user press ctrl+c
select {
case <-r.ctx.Done():
return false
case <-ctx.Done():
return false
default:
}
// we try recover when encountered any error that is not telegram business error
_, ok := tgerr.As(err)
return !ok
}
================================================
FILE: core/middlewares/retry/retry.go
================================================
package retry
import (
"context"
"fmt"
"github.com/go-faster/errors"
"github.com/gotd/td/bin"
"github.com/gotd/td/telegram"
"github.com/gotd/td/tg"
"github.com/gotd/td/tgerr"
"go.uber.org/zap"
"github.com/iyear/tdl/core/logctx"
)
var internalErrors = []string{
"Timedout", // #373
"No workers running",
"RPC_CALL_FAIL",
"RPC_MCGET_FAIL",
"WORKER_BUSY_TOO_LONG_RETRY", // #462
"memory limit exit", // #504
}
type retry struct {
max int
errors []string
}
func (r retry) Handle(next tg.Invoker) telegram.InvokeFunc {
return func(ctx context.Context, input bin.Encoder, output bin.Decoder) error {
retries := 0
for retries < r.max {
if err := next.Invoke(ctx, input, output); err != nil {
if tgerr.Is(err, r.errors...) {
logctx.From(ctx).Debug("retry middleware", zap.Int("retries", retries), zap.Error(err))
retries++
continue
}
return errors.Wrap(err, "retry middleware skip")
}
return nil
}
return fmt.Errorf("retry limit reached after %d attempts", r.max)
}
}
// New returns middleware that retries request if it fails with one of provided errors.
func New(max int, errors ...string) telegram.Middleware {
return retry{
max: max,
errors: append(errors, internalErrors...), // #373
}
}
================================================
FILE: core/middlewares/takeout/middleware.go
================================================
package takeout
import (
"context"
"errors"
"github.com/gotd/td/bin"
"github.com/gotd/td/telegram"
"github.com/gotd/td/tg"
)
type takeout struct {
id int64
}
type nopDecoder struct {
bin.Encoder
}
func (n nopDecoder) Decode(_ *bin.Buffer) error {
return errors.New("bin.Decoder is not implemented")
}
func (t takeout) Handle(next tg.Invoker) telegram.InvokeFunc {
return func(ctx context.Context, input bin.Encoder, output bin.Decoder) error {
return next.Invoke(ctx, &tg.InvokeWithTakeoutRequest{
TakeoutID: t.id,
Query: nopDecoder{input},
}, output)
}
}
func Middleware(id int64) telegram.Middleware {
return takeout{id: id}
}
================================================
FILE: core/middlewares/takeout/takeout.go
================================================
package takeout
import (
"context"
"github.com/gotd/td/tg"
)
func Takeout(ctx context.Context, invoker tg.Invoker) (int64, error) {
req := &tg.AccountInitTakeoutSessionRequest{
Contacts: true,
MessageUsers: true,
MessageChats: true,
MessageMegagroups: true,
MessageChannels: true,
Files: true,
FileMaxSize: 4000 * 1024 * 1024,
}
req.SetFlags()
session, err := tg.NewClient(invoker).AccountInitTakeoutSession(ctx, req)
if err != nil {
return 0, err
}
return session.ID, nil
}
// UnTakeout should be called with takeout wrapper invoker
func UnTakeout(ctx context.Context, invoker tg.Invoker) error {
req := &tg.AccountFinishTakeoutSessionRequest{Success: true}
req.SetFlags()
_, err := tg.NewClient(invoker).AccountFinishTakeoutSession(ctx, req)
return err
}
================================================
FILE: core/storage/keygen/keygen.go
================================================
package keygen
import (
"bytes"
"strings"
"sync"
)
var keyPool = sync.Pool{
New: func() interface{} {
b := &bytes.Buffer{}
b.Grow(16)
return b
},
}
func New(indexes ...string) string {
buf := keyPool.Get().(*bytes.Buffer)
buf.WriteString(strings.Join(indexes, ":"))
t := buf.String()
buf.Reset()
keyPool.Put(buf)
return t
}
================================================
FILE: core/storage/peers.go
================================================
package storage
import (
"context"
"encoding/json"
"errors"
"strconv"
"github.com/gotd/td/telegram/peers"
"github.com/iyear/tdl/core/storage/keygen"
)
type Peers struct {
kv Storage
}
func NewPeers(kv Storage) peers.Storage {
return &Peers{kv: kv}
}
func (p *Peers) Save(ctx context.Context, key peers.Key, value peers.Value) error {
bytes, err := json.Marshal(value)
if err != nil {
return err
}
return p.kv.Set(ctx, p.key(key), bytes)
}
func (p *Peers) Find(ctx context.Context, key peers.Key) (peers.Value, bool, error) {
data, err := p.kv.Get(ctx, p.key(key))
if err != nil {
if errors.Is(err, ErrNotFound) {
return peers.Value{}, false, nil
}
return peers.Value{}, false, err
}
var value peers.Value
if err = json.Unmarshal(data, &value); err != nil {
return peers.Value{}, false, err
}
return value, true, nil
}
func (p *Peers) SavePhone(ctx context.Context, phone string, _key peers.Key) error {
bytes, err := json.Marshal(_key)
if err != nil {
return err
}
return p.kv.Set(ctx, p.phoneKey(phone), bytes)
}
func (p *Peers) FindPhone(ctx context.Context, phone string) (peers.Key, peers.Value, bool, error) {
data, err := p.kv.Get(ctx, p.phoneKey(phone))
if err != nil {
if errors.Is(err, ErrNotFound) {
return peers.Key{}, peers.Value{}, false, nil
}
return peers.Key{}, peers.Value{}, false, err
}
var _key peers.Key
if err = json.Unmarshal(data, &_key); err != nil {
return peers.Key{}, peers.Value{}, false, err
}
value, found, err := p.Find(ctx, _key)
if err != nil {
return peers.Key{}, peers.Value{}, false, err
}
return _key, value, found, nil
}
func (p *Peers) GetContactsHash(ctx context.Context) (int64, error) {
data, err := p.kv.Get(ctx, p.contactsKey())
if err != nil {
if errors.Is(err, ErrNotFound) {
return 0, nil
}
return 0, err
}
return strconv.ParseInt(string(data), 10, 64)
}
func (p *Peers) SaveContactsHash(ctx context.Context, hash int64) error {
return p.kv.Set(ctx, p.contactsKey(), []byte(strconv.FormatInt(hash, 10)))
}
func (p *Peers) key(key peers.Key) string {
return keygen.New("peers", "key", key.Prefix, strconv.FormatInt(key.ID, 10))
}
func (p *Peers) phoneKey(phone string) string {
return keygen.New("peers", "phone", phone)
}
func (p *Peers) contactsKey() string {
return keygen.New("peers", "contacts", "hash")
}
================================================
FILE: core/storage/session.go
================================================
package storage
import (
"context"
"errors"
"github.com/gotd/td/telegram"
"github.com/iyear/tdl/core/storage/keygen"
)
type Session struct {
kv Storage
login bool
}
func NewSession(kv Storage, login bool) telegram.SessionStorage {
return &Session{kv: kv, login: login}
}
func (s *Session) LoadSession(ctx context.Context) ([]byte, error) {
if s.login {
return nil, nil
}
b, err := s.kv.Get(ctx, s.key())
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, nil
}
return nil, err
}
return b, nil
}
func (s *Session) StoreSession(ctx context.Context, data []byte) error {
return s.kv.Set(ctx, s.key(), data)
}
func (s *Session) key() string {
return keygen.New("session")
}
================================================
FILE: core/storage/state.go
================================================
package storage
import (
"context"
"encoding/json"
"errors"
"strconv"
"github.com/gotd/td/telegram/updates"
"github.com/iyear/tdl/core/storage/keygen"
)
type State struct {
kv Storage
}
func NewState(kv Storage) updates.StateStorage {
return &State{kv: kv}
}
func (s *State) Get(ctx context.Context, key string, v interface{}) error {
data, err := s.kv.Get(ctx, key)
if err != nil {
return err
}
return json.Unmarshal(data, v)
}
func (s *State) Set(ctx context.Context, key string, v interface{}) error {
data, err := json.Marshal(v)
if err != nil {
return err
}
return s.kv.Set(ctx, key, data)
}
func (s *State) GetState(ctx context.Context, userID int64) (updates.State, bool, error) {
state := updates.State{}
if err := s.Get(ctx, s.stateKey(userID), &state); err != nil {
if errors.Is(err, ErrNotFound) {
return state, false, nil
}
return state, false, err
}
return state, true, nil
}
func (s *State) SetState(ctx context.Context, userID int64, state updates.State) error {
if err := s.Set(ctx, s.stateKey(userID), state); err != nil {
return err
}
return s.Set(ctx, s.channelKey(userID), struct{}{})
}
func (s *State) SetPts(ctx context.Context, userID int64, pts int) error {
state, k := updates.State{}, s.stateKey(userID)
if err := s.Get(ctx, k, &state); err != nil {
return err
}
state.Pts = pts
return s.Set(ctx, k, state)
}
func (s *State) SetQts(ctx context.Context, userID int64, qts int) error {
state, k := updates.State{}, s.stateKey(userID)
if err := s.Get(ctx, k, &state); err != nil {
return err
}
state.Qts = qts
return s.Set(ctx, k, state)
}
func (s *State) SetDate(ctx context.Context, userID int64, date int) error {
state, k := updates.State{}, s.stateKey(userID)
if err := s.Get(ctx, k, &state); err != nil {
return err
}
state.Date = date
return s.Set(ctx, k, state)
}
func (s *State) SetSeq(ctx context.Context, userID int64, seq int) error {
state, k := updates.State{}, s.stateKey(userID)
if err := s.Get(ctx, k, &state); err != nil {
return err
}
state.Seq = seq
return s.Set(ctx, k, state)
}
func (s *State) SetDateSeq(ctx context.Context, userID int64, date, seq int) error {
state, k := updates.State{}, s.stateKey(userID)
if err := s.Get(ctx, k, &state); err != nil {
return err
}
state.Date = date
state.Seq = seq
return s.Set(ctx, k, state)
}
func (s *State) GetChannelPts(ctx context.Context, userID, channelID int64) (int, bool, error) {
c := make(map[int64]int)
if err := s.Get(ctx, s.channelKey(userID), &c); err != nil {
if errors.Is(err, ErrNotFound) {
return 0, false, nil
}
return 0, false, err
}
pts, ok := c[channelID]
if !ok {
return 0, false, nil
}
return pts, true, nil
}
func (s *State) SetChannelPts(ctx context.Context, userID, channelID int64, pts int) error {
c, k := make(map[int64]int), s.channelKey(userID)
if err := s.Get(ctx, k, &c); err != nil {
return err
}
c[channelID] = pts
return s.Set(ctx, k, c)
}
func (s *State) ForEachChannels(ctx context.Context, userID int64, f func(ctx context.Context, channelID int64, pts int) error) error {
c := make(map[int64]int)
if err := s.Get(ctx, s.channelKey(userID), &c); err != nil {
return err
}
for channelID, pts := range c {
if err := f(ctx, channelID, pts); err != nil {
return err
}
}
return nil
}
func (s *State) stateKey(userID int64) string {
return keygen.New("state", strconv.FormatInt(userID, 10))
}
func (s *State) channelKey(userID int64) string {
return keygen.New("chan", strconv.FormatInt(userID, 10))
}
================================================
FILE: core/storage/storage.go
================================================
package storage
import (
"context"
"github.com/go-faster/errors"
)
type Storage interface {
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, value []byte) error
Delete(ctx context.Context, key string) error
}
var ErrNotFound = errors.New("key not found")
================================================
FILE: core/tclient/tclient.go
================================================
package tclient
import (
"context"
"fmt"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/go-faster/errors"
"github.com/gotd/contrib/clock"
"github.com/gotd/contrib/middleware/floodwait"
tdclock "github.com/gotd/td/clock"
"github.com/gotd/td/exchange"
"github.com/gotd/td/telegram"
"github.com/gotd/td/telegram/dcs"
"golang.org/x/net/proxy"
"github.com/iyear/tdl/core/logctx"
"github.com/iyear/tdl/core/middlewares/recovery"
"github.com/iyear/tdl/core/middlewares/retry"
"github.com/iyear/tdl/core/util/netutil"
"github.com/iyear/tdl/core/util/tutil"
)
// dc values can be overridden globally
var (
DCList dcs.List
DC int
PublicKeys []exchange.PublicKey
)
type Options struct {
AppID int
AppHash string
Session telegram.SessionStorage
Middlewares []telegram.Middleware
Proxy string
NTP string
ReconnectTimeout time.Duration
UpdateHandler telegram.UpdateHandler
}
// New creates new telegram client with given options.
// Default middlewares(retry, recovery, flood wait) always added.
func New(ctx context.Context, o Options) (*telegram.Client, error) {
// process clock
tclock := tdclock.System
if ntp := o.NTP; ntp != "" {
var err error
tclock, err = clock.NewNTP(ntp)
if err != nil {
return nil, errors.Wrap(err, "create network clock")
}
}
// process proxy
var dialer dcs.DialFunc = proxy.Direct.DialContext
if p := o.Proxy; p != "" {
d, err := netutil.NewProxy(p)
if err != nil {
return nil, errors.Wrap(err, "get dialer")
}
dialer = d.DialContext
}
opts := telegram.Options{
Resolver: dcs.Plain(dcs.PlainOptions{
Dial: dialer,
}),
ReconnectionBackoff: func() backoff.BackOff {
return newBackoff(o.ReconnectTimeout)
},
DC: DC,
DCList: DCList,
PublicKeys: PublicKeys,
UpdateHandler: o.UpdateHandler,
Device: tutil.Device,
SessionStorage: o.Session,
RetryInterval: 5 * time.Second,
MaxRetries: 5,
DialTimeout: 10 * time.Second,
Middlewares: append(NewDefaultMiddlewares(ctx, o.ReconnectTimeout), o.Middlewares...),
Clock: tclock,
Logger: logctx.From(ctx).Named("td"),
}
return telegram.NewClient(o.AppID, o.AppHash, opts), nil
}
func NewDefaultMiddlewares(ctx context.Context, timeout time.Duration) []telegram.Middleware {
return []telegram.Middleware{
recovery.New(ctx, newBackoff(timeout)),
retry.New(5),
floodwait.NewSimpleWaiter(),
}
}
func newBackoff(timeout time.Duration) backoff.BackOff {
b := backoff.NewExponentialBackOff()
b.Multiplier = 1.1
b.MaxElapsedTime = timeout
b.MaxInterval = 10 * time.Second
return b
}
func RunWithAuth(ctx context.Context, client *telegram.Client, f func(ctx context.Context) error) error {
return client.Run(ctx, func(ctx context.Context) error {
status, err := client.Auth().Status(ctx)
if err != nil {
return err
}
if !status.Authorized {
return fmt.Errorf("not authorized. please login first")
}
return f(ctx)
})
}
================================================
FILE: core/tmedia/convert.go
================================================
package tmedia
import (
"github.com/gotd/td/tg"
)
func ConvInputMedia(media tg.MessageMediaClass) (tg.InputMediaClass, bool) {
switch v := media.(type) {
case *tg.MessageMediaPhoto:
return ConvInputMediaPhoto(v)
case *tg.MessageMediaGeo:
return ConvInputMediaGeo(v)
case *tg.MessageMediaContact:
return ConvInputMediaContact(v)
case *tg.MessageMediaDocument:
return ConvInputMediaDocument(v)
case *tg.MessageMediaVenue:
return ConvInputMediaVenue(v)
case *tg.MessageMediaGame:
return ConvInputMediaGame(v)
case *tg.MessageMediaInvoice:
return ConvInputMediaInvoice(v)
case *tg.MessageMediaGeoLive:
return ConvInputMediaGeoLive(v)
case *tg.MessageMediaPoll:
return ConvInputMediaPoll(v)
case *tg.MessageMediaDice:
return ConvInputMediaDice(v)
case *tg.MessageMediaStory:
return ConvInputMediaStory(v)
case *tg.MessageMediaUnsupported:
return nil, false
default:
return nil, false
}
}
func ConvInputMediaPhoto(v *tg.MessageMediaPhoto) (*tg.InputMediaPhoto, bool) {
switch t := v.Photo.(type) {
case *tg.PhotoEmpty:
return nil, false
case *tg.Photo:
p := &tg.InputPhoto{}
p.FillFrom(t)
ret := &tg.InputMediaPhoto{
Spoiler: v.Spoiler,
ID: p,
TTLSeconds: v.TTLSeconds,
}
ret.SetFlags()
return ret, true
default:
return nil, false
}
}
func ConvInputMediaGeo(v *tg.MessageMediaGeo) (*tg.InputMediaGeoPoint, bool) {
switch t := v.Geo.(type) {
case *tg.GeoPointEmpty:
return nil, false
case *tg.GeoPoint:
g := &tg.InputGeoPoint{}
g.FillFrom(t)
g.SetFlags()
return &tg.InputMediaGeoPoint{
GeoPoint: g,
}, true
default:
return nil, false
}
}
func ConvInputMediaContact(v *tg.MessageMediaContact) (*tg.InputMediaContact, bool) {
c := &tg.InputMediaContact{}
c.FillFrom(v)
return c, true
}
func ConvInputMediaDocument(v *tg.MessageMediaDocument) (*tg.InputMediaDocument, bool) {
switch t := v.Document.(type) {
case *tg.DocumentEmpty:
return nil, false
case *tg.Document:
d := &tg.InputDocument{}
d.FillFrom(t)
ret := &tg.InputMediaDocument{
Spoiler: v.Spoiler,
ID: d,
TTLSeconds: v.TTLSeconds,
}
ret.SetFlags()
return ret, true
default:
return nil, false
}
}
func ConvInputMediaVenue(v *tg.MessageMediaVenue) (*tg.InputMediaVenue, bool) {
geo, ok := ConvInputMediaGeo(&tg.MessageMediaGeo{Geo: v.Geo})
if !ok {
return nil, false
}
return &tg.InputMediaVenue{
GeoPoint: geo.GeoPoint,
Title: v.Title,
Address: v.Address,
Provider: v.Provider,
VenueID: v.VenueID,
VenueType: v.VenueType,
}, true
}
func ConvInputMediaGame(v *tg.MessageMediaGame) (*tg.InputMediaGame, bool) {
g := &tg.InputGameID{}
g.FillFrom(&v.Game)
return &tg.InputMediaGame{
ID: g,
}, true
}
func ConvInputMediaInvoice(v *tg.MessageMediaInvoice) (*tg.InputMediaInvoice, bool) {
// TODO(iyear): unsupported
_ = v
return nil, false
}
func ConvInputMediaGeoLive(v *tg.MessageMediaGeoLive) (*tg.InputMediaGeoLive, bool) {
// TODO(): unsupported
_ = v
return nil, false
}
func ConvInputMediaPoll(v *tg.MessageMediaPoll) (*tg.InputMediaPoll, bool) {
// TODO(): unsupported
_ = v
return nil, false
}
func ConvInputMediaDice(v *tg.MessageMediaDice) (*tg.InputMediaDice, bool) {
return &tg.InputMediaDice{
Emoticon: v.Emoticon,
}, true
}
func ConvInputMediaStory(v *tg.MessageMediaStory) (*tg.InputMediaStory, bool) {
// TODO(): unsupported
_ = v
return nil, false
}
================================================
FILE: core/tmedia/document.go
================================================
package tmedia
import (
"strconv"
"github.com/gabriel-vasile/mimetype"
"github.com/gotd/td/tg"
)
func GetDocumentInfo(doc *tg.MessageMediaDocument) (*Media, bool) {
d, ok := doc.Document.(*tg.Document)
if !ok {
return nil, false
}
return &Media{
InputFileLoc: &tg.InputDocumentFileLocation{
ID: d.ID,
AccessHash: d.AccessHash,
FileReference: d.FileReference,
},
Name: GetDocumentName(d),
Size: d.Size,
DC: d.DCID,
Date: int64(d.Date),
}, true
}
func GetDocumentName(doc *tg.Document) string {
for _, attr := range doc.Attributes {
name, ok := attr.(*tg.DocumentAttributeFilename)
if ok {
return name.FileName
}
}
// #185: stable file name so --skip-same can work
mime := mimetype.Lookup(doc.MimeType)
ext := ".unknown"
if mime != nil {
ext = mime.Extension()
}
return strconv.FormatInt(doc.ID, 10) + ext
}
================================================
FILE: core/tmedia/media.go
================================================
package tmedia
import (
"github.com/gotd/td/tg"
)
type Media struct {
InputFileLoc tg.InputFileLocationClass // mtproto file location of the media file
Name string // file name
Size int64 // size in bytes
DC int // which DC the media is stored
Date int64 // media creation(upload) timestamp
}
func ExtractMedia(m tg.MessageMediaClass) (*Media, bool) {
switch m := m.(type) {
case *tg.MessageMediaPhoto:
return GetPhotoInfo(m)
case *tg.MessageMediaDocument:
return GetDocumentInfo(m)
case *tg.MessageMediaInvoice:
return GetExtendedMedia(m.ExtendedMedia)
}
return nil, false
}
func GetMedia(msg tg.MessageClass) (*Media, bool) {
mm, ok := msg.(*tg.Message)
if !ok {
return nil, false
}
media, ok := mm.GetMedia()
if !ok {
return nil, false
}
return ExtractMedia(media)
}
func GetExtendedMedia(mm tg.MessageExtendedMediaClass) (*Media, bool) {
m, ok := mm.(*tg.MessageExtendedMedia)
if !ok {
return nil, false
}
return ExtractMedia(m.Media)
}
func GetDocumentThumb(doc *tg.Document) (*Media, bool) {
thumbs, exists := doc.GetThumbs()
if !exists {
return nil, false
}
photoSize := &tg.PhotoSize{}
for _, t := range thumbs {
if p, ok := t.(*tg.PhotoSize); ok {
photoSize = p
break
}
}
if photoSize == nil {
return nil, false
}
return &Media{
InputFileLoc: &tg.InputDocumentFileLocation{
ID: doc.ID,
AccessHash: doc.AccessHash,
FileReference: doc.FileReference,
ThumbSize: photoSize.Type,
},
Name: "thumb.jpg",
Size: int64(photoSize.Size),
DC: doc.DCID,
Date: int64(doc.Date),
}, true
}
================================================
FILE: core/tmedia/photo.go
================================================
package tmedia
import (
"strconv"
"github.com/gotd/td/tg"
)
func GetPhotoInfo(photo *tg.MessageMediaPhoto) (*Media, bool) {
p, ok := photo.Photo.(*tg.Photo)
if !ok {
return nil, false
}
tp, size, ok := GetPhotoSize(p.Sizes)
if !ok {
return nil, false
}
return &Media{
InputFileLoc: &tg.InputPhotoFileLocation{
ID: p.ID,
AccessHash: p.AccessHash,
FileReference: p.FileReference,
ThumbSize: tp,
},
// Telegram photo is compressed, and extension is always jpg.
Name: strconv.FormatInt(p.ID, 10) + ".jpg", // unique name
Size: int64(size),
DC: p.DCID,
Date: int64(p.Date),
}, true
}
func GetPhotoSize(sizes []tg.PhotoSizeClass) (string, int, bool) {
size := sizes[len(sizes)-1]
switch s := size.(type) {
case *tg.PhotoSize:
return s.Type, s.Size, true
case *tg.PhotoSizeProgressive:
return s.Type, s.Sizes[len(s.Sizes)-1], true
}
return "", 0, false
}
================================================
FILE: core/uploader/iter.go
================================================
package uploader
import (
"context"
"io"
"github.com/gotd/td/tg"
)
type Iter interface {
Next(ctx context.Context) bool
Value() Elem
Err() error
}
type File interface {
io.ReadSeeker
Name() string
Size() int64
}
type Elem interface {
File() File
Thumb() (File, bool)
Caption() (string, []tg.MessageEntityClass)
To() tg.InputPeerClass
Thread() int
AsPhoto() bool
}
================================================
FILE: core/uploader/progress.go
================================================
package uploader
import (
"context"
"github.com/gotd/td/telegram/uploader"
)
type Progress interface {
OnAdd(elem Elem)
OnUpload(elem Elem, state ProgressState)
OnDone(elem Elem, err error)
// TODO: OnLog to log something that is not an error but should be sent to the user
}
type ProgressState struct {
Uploaded int64
Total int64
}
type wrapProcess struct {
elem Elem
process Progress
}
func (p *wrapProcess) Chunk(_ context.Context, state uploader.ProgressState) error {
p.process.OnUpload(p.elem, ProgressState{
Uploaded: state.Uploaded,
Total: state.Total,
})
return nil
}
================================================
FILE: core/uploader/uploader.go
================================================
package uploader
import (
"context"
"io"
"time"
"github.com/gabriel-vasile/mimetype"
"github.com/go-faster/errors"
"github.com/gotd/td/telegram/message"
"github.com/gotd/td/telegram/message/entity"
"github.com/gotd/td/telegram/message/styling"
"github.com/gotd/td/telegram/uploader"
"github.com/gotd/td/tg"
"github.com/samber/lo"
"golang.org/x/sync/errgroup"
"github.com/iyear/tdl/core/util/fsutil"
"github.com/iyear/tdl/core/util/mediautil"
)
// MaxPartSize refer to https://core.telegram.org/api/files#uploading-files
const MaxPartSize = 512 * 1024
type Uploader struct {
opts Options
}
type Options struct {
Client *tg.Client
Threads int
Iter Iter
Progress Progress
}
func New(o Options) *Uploader {
return &Uploader{opts: o}
}
func (u *Uploader) Upload(ctx context.Context, limit int) error {
wg, wgctx := errgroup.WithContext(ctx)
wg.SetLimit(limit)
for u.opts.Iter.Next(wgctx) {
elem := u.opts.Iter.Value()
wg.Go(func() (rerr error) {
u.opts.Progress.OnAdd(elem)
defer func() { u.opts.Progress.OnDone(elem, rerr) }()
if err := u.upload(wgctx, elem); err != nil {
// canceled by user, so we directly return error to stop all
if errors.Is(err, context.Canceled) {
return errors.Wrap(err, "upload")
}
// don't return error, just log it
}
return nil
})
}
if err := u.opts.Iter.Err(); err != nil {
return errors.Wrap(err, "iter")
}
return wg.Wait()
}
func (u *Uploader) upload(ctx context.Context, elem Elem) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
up := uploader.NewUploader(u.opts.Client).
WithPartSize(MaxPartSize).
WithThreads(u.opts.Threads).
WithProgress(&wrapProcess{
elem: elem,
process: u.opts.Progress,
})
f, err := up.Upload(ctx, uploader.NewUpload(elem.File().Name(), elem.File(), elem.File().Size()))
if err != nil {
return errors.Wrap(err, "upload file")
}
if _, err = elem.File().Seek(0, io.SeekStart); err != nil {
return errors.Wrap(err, "seek file")
}
mime, err := mimetype.DetectReader(elem.File())
if err != nil {
return errors.Wrap(err, "detect mime")
}
// here convert underlying entities to formatters for message caption
caption := styling.Custom(func(eb *entity.Builder) error {
msg, entities := elem.Caption()
eb.Format(msg, lo.Map(entities, func(item tg.MessageEntityClass, _ int) entity.Formatter {
return func(_, _ int) tg.MessageEntityClass {
return item
}
})...)
return nil
})
doc := message.UploadedDocument(f, caption).MIME(mime.String()).Filename(elem.File().Name())
// upload thumbnail TODO(iyear): maybe still unavailable
if thumb, ok := elem.Thumb(); ok {
if thumbFile, err := uploader.NewUploader(u.opts.Client).
FromReader(ctx, thumb.Name(), thumb); err == nil {
doc = doc.Thumb(thumbFile)
}
}
var media message.MediaOption = doc
switch {
case mediautil.IsImage(mime.String()) && elem.AsPhoto():
// webp should be uploaded as document
if mime.String() == "image/webp" {
break
}
// upload as photo
media = message.UploadedPhoto(f, caption)
case mediautil.IsVideo(mime.String()):
// reset reader
if _, err = elem.File().Seek(0, io.SeekStart); err != nil {
return errors.Wrap(err, "seek file")
}
if dur, w, h, err := mediautil.GetMP4Info(elem.File()); err == nil {
// #132. There may be some errors, but we can still upload the file
media = doc.Video().
Duration(time.Duration(dur)*time.Second).
Resolution(w, h).
SupportsStreaming()
}
case mediautil.IsAudio(mime.String()):
media = doc.Audio().Title(fsutil.GetNameWithoutExt(elem.File().Name()))
}
_, err = message.NewSender(u.opts.Client).
WithUploader(up).
To(elem.To()).
Reply(elem.Thread()).
Media(ctx, media)
if err != nil {
return errors.Wrap(err, "send message")
}
return nil
}
================================================
FILE: core/util/fsutil/fsutil.go
================================================
package fsutil
import (
"os"
"path/filepath"
"strings"
)
func GetNameWithoutExt(path string) string {
return strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
}
func PathExists(path string) bool {
_, err := os.Stat(path)
return err == nil || os.IsExist(err)
}
// AddPrefixDot add prefix dot if extension don't have
func AddPrefixDot(ext string) string {
if !strings.HasPrefix(ext, ".") {
return "." + ext
}
return ext
}
================================================
FILE: core/util/logutil/logutil.go
================================================
package logutil
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
func New(level zapcore.LevelEnabler, path string) *zap.Logger {
rotate := &lumberjack.Logger{
Filename: path,
MaxSize: 10,
MaxAge: 7,
MaxBackups: 3,
LocalTime: true,
Compress: true,
}
writer := zapcore.AddSync(rotate)
config := zap.NewDevelopmentEncoderConfig()
config.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05")
config.EncodeLevel = zapcore.CapitalLevelEncoder
core := zapcore.NewCore(zapcore.NewConsoleEncoder(config), writer, level)
return zap.New(core, zap.AddCaller())
}
================================================
FILE: core/util/mediautil/mediautil.go
================================================
package mediautil
import (
"fmt"
"io"
"strings"
"github.com/yapingcat/gomedia/go-mp4"
)
func split(mime string) (primary string, sub string, ok bool) {
types := strings.Split(mime, "/")
if len(types) != 2 {
return "", "", false
}
return types[0], types[1], true
}
func IsVideo(mime string) bool {
primary, _, ok := split(mime)
return primary == "video" && ok
}
func IsAudio(mime string) bool {
primary, _, ok := split(mime)
return primary == "audio" && ok
}
func IsImage(mime string) bool {
primary, _, ok := split(mime)
return primary == "image" && ok
}
// GetMP4Info returns duration, width, height, error
func GetMP4Info(r io.ReadSeeker) (int, int, int, error) {
d := mp4.CreateMp4Demuxer(r)
tracks, err := d.ReadHead()
if err != nil {
return 0, 0, 0, err
}
for _, track := range tracks {
if track.Cid == mp4.MP4_CODEC_H264 {
info := d.GetMp4Info()
return int(info.Duration / info.Timescale), int(track.Width), int(track.Height), nil
}
}
return 0, 0, 0, fmt.Errorf("no h264 track found")
}
================================================
FILE: core/util/netutil/netutil.go
================================================
package netutil
import (
"net/url"
"github.com/go-faster/errors"
"github.com/iyear/connectproxy"
"golang.org/x/net/proxy"
)
func init() {
connectproxy.Register(&connectproxy.Config{
InsecureSkipVerify: true,
})
}
func NewProxy(proxyUrl string) (proxy.ContextDialer, error) {
u, err := url.Parse(proxyUrl)
if err != nil {
return nil, errors.Wrap(err, "parse proxy url")
}
dialer, err := proxy.FromURL(u, proxy.Direct)
if err != nil {
return nil, errors.Wrap(err, "proxy from url")
}
if d, ok := dialer.(proxy.ContextDialer); ok {
return d, nil
}
return nil, errors.New("proxy dialer is not ContextDialer")
}
================================================
FILE: core/util/tutil/device.go
================================================
package tutil
import "github.com/gotd/td/telegram"
var Device = telegram.DeviceConfig{
DeviceModel: "Desktop",
SystemVersion: "Windows 10",
AppVersion: "4.2.4 x64",
LangCode: "en",
SystemLangCode: "en-US",
LangPack: "tdesktop",
}
================================================
FILE: core/util/tutil/tutil.go
================================================
package tutil
import (
"context"
"fmt"
"net/url"
"strconv"
"strings"
"github.com/go-faster/errors"
"github.com/gotd/td/telegram/peers"
"github.com/gotd/td/telegram/query"
"github.com/gotd/td/tg"
)
// ErrMessageDeleted is returned when a message is detected as deleted.
var ErrMessageDeleted = errors.New("message may be deleted")
// ParseMessageLink return dialog id, msg id, error
func ParseMessageLink(ctx context.Context, manager *peers.Manager, s string) (peers.Peer, int, error) {
parse := func(from, msg string) (peers.Peer, int, error) {
ch, err := GetInputPeer(ctx, manager, from)
if err != nil {
return nil, 0, errors.Wrap(err, "input peer")
}
m, err := strconv.Atoi(msg)
if err != nil {
return nil, 0, errors.Wrap(err, "parse message id")
}
return ch, m, nil
}
u, err := url.Parse(s)
if err != nil {
return nil, 0, err
}
paths := strings.Split(strings.TrimPrefix(u.Path, "/"), "/")
// https://t.me/opencfdchannel/4434?comment=360409
if c := u.Query().Get("comment"); c != "" {
peer, err := GetInputPeer(ctx, manager, paths[0])
if err != nil {
return nil, 0, errors.Wrap(err, "input peer")
}
ch, ok := peer.(peers.Channel)
if !ok || !ch.IsBroadcast() {
return nil, 0, errors.New("not channel")
}
raw, err := ch.FullRaw(ctx)
if err != nil {
return nil, 0, errors.Wrap(err, "full raw")
}
linked, ok := raw.GetLinkedChatID()
if !ok {
return nil, 0, errors.New("no linked chat")
}
return parse(strconv.FormatInt(linked, 10), c)
}
switch len(paths) {
case 2:
// https://t.me/telegram/193
// https://t.me/myhostloc/1485524?thread=1485523
return parse(paths[0], paths[1])
case 3:
// https://t.me/c/1697797156/151
// https://t.me/iFreeKnow/45662/55005
if paths[0] == "c" {
return parse(paths[1], paths[2])
}
// "45662" means topic id, we don't need it
return parse(paths[0], paths[2])
case 4:
// https://t.me/c/1492447836/251015/251021
if paths[0] != "c" {
return nil, 0, fmt.Errorf("invalid message link")
}
// "251015" means topic id, we don't need it
return parse(paths[1], paths[3])
default:
return nil, 0, fmt.Errorf("invalid message link: %s", s)
}
}
func GetInputPeer(ctx context.Context, manager *peers.Manager, from string) (peers.Peer, error) {
id, err := strconv.ParseInt(from, 10, 64)
if err != nil {
// from is username
p, err := manager.Resolve(ctx, from)
if err != nil {
return nil, err
}
return p, nil
}
var p peers.Peer
if p, err = manager.ResolveChannelID(ctx, id); err == nil {
return p, nil
}
if p, err = manager.ResolveUserID(ctx, id); err == nil {
return p, nil
}
if p, err = manager.ResolveChatID(ctx, id); err == nil {
return p, nil
}
return nil, fmt.Errorf("failed to get result from %d:%v", id, err)
}
func GetPeerID(peer tg.PeerClass) int64 {
switch p := peer.(type) {
case *tg.PeerUser:
return p.UserID
case *tg.PeerChat:
return p.ChatID
case *tg.PeerChannel:
return p.ChannelID
}
return 0
}
func GetInputPeerID(peer tg.InputPeerClass) int64 {
switch p := peer.(type) {
case *tg.InputPeerUser:
return p.UserID
case *tg.InputPeerChat:
return p.ChatID
case *tg.InputPeerChannel:
return p.ChannelID
}
return 0
}
func GetBlockedDialogs(ctx context.Context, client *tg.Client) (map[int64]struct{}, error) {
blocks, err := query.GetBlocked(client).BatchSize(100).Collect(ctx)
if err != nil {
return nil, err
}
blockids := make(map[int64]struct{})
for _, b := range blocks {
blockids[GetPeerID(b.Contact.PeerID)] = struct{}{}
}
return blockids, nil
}
func FileExists(msg tg.MessageClass) bool {
m, ok := msg.(*tg.Message)
if !ok {
return false
}
md, ok := m.GetMedia()
if !ok {
return false
}
switch md.(type) {
case *tg.MessageMediaDocument, *tg.MessageMediaPhoto:
return true
default:
return false
}
}
func GetSingleMessage(ctx context.Context, c *tg.Client, peer tg.InputPeerClass, msg int) (*tg.Message, error) {
it := query.Messages(c).
GetHistory(peer).OffsetID(msg + 1).
BatchSize(1).Iter()
if !it.Next(ctx) {
return nil, errors.Wrap(it.Err(), "get single message")
}
m, ok := it.Value().Msg.(*tg.Message)
if !ok {
return nil, errors.Errorf("invalid message %d", msg)
}
// check if message is deleted
if m.GetID() != msg {
return nil, fmt.Errorf("the message %d/%d: %w", GetInputPeerID(peer), msg, ErrMessageDeleted)
}
return m, nil
}
type Messages []*tg.Message
func (m Messages) Len() int {
return len(m)
}
func (m Messages) Less(i, j int) bool {
return m[i].ID < m[j].ID
}
func (m Messages) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
func GetGroupedMessages(ctx context.Context, c *tg.Client, peer tg.InputPeerClass, msg *tg.Message) ([]*tg.Message, error) {
group, ok := msg.GetGroupedID()
if !ok {
return nil, errors.New("not grouped message")
}
// https://telegram.org/blog/albums-saved-messages
// Each album can include up to 10 photos or videos
batchSize := 20
it := query.Messages(c).GetHistory(peer).
OffsetID(msg.ID + 11). // from latest to oldest
BatchSize(batchSize).Iter()
messages := make([]*tg.Message, 0, batchSize)
for i := 0; it.Next(ctx) && i < batchSize; i++ {
m, ok := it.Value().Msg.(*tg.Message)
if !ok {
continue
}
groupID, ok := m.GetGroupedID()
if !ok {
continue
}
if groupID != group {
continue
}
// append argument msg to the end of messages because of it may have been modified.
// Like forward edit flag.
if m.ID == msg.ID {
messages = append(messages, msg)
} else {
messages = append(messages, m)
}
}
// reverse messages from oldest to latest, so we can forward them in order
for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
messages[i], messages[j] = messages[j], messages[i]
}
return messages, nil
}
var threadsLevels = []struct {
threads int
size int64
}{
{1, 1 << 20},
{2, 5 << 20},
{4, 20 << 20},
{8, 50 << 20},
}
func BestThreads(size int64, max int) int {
// Get best threads num for download, based on file size
for _, thread := range threadsLevels {
if size < thread.size {
return min(thread.threads, max)
}
}
return max
}
================================================
FILE: docs/assets/_custom.scss
================================================
@import "plugins/_scrollbars.scss";
.markdown pre {
outline: none;
}
.command::before {
content: attr(prompt);
opacity: .7;
display: inline;
padding-right: 0.7em;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace;
border-right: 1px solid #999;
margin-right: 0.6em;
letter-spacing: -1px;
font-size: 105%;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
pointer-events: none;
}
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background-color: #494949;
color: #fff;
padding: 10px;
border-radius: 8px;
z-index: 9999; // top
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
================================================
FILE: docs/content/en/_index.md
================================================
---
title: Introduction
---
# tdl





{{< image src="img/logo.png" align="right" height="270" width="270">}}
📥 Telegram Downloader, but more than a downloader
#### Features:
- Single file start-up
- Low resource usage
- Take up all your bandwidth
- Faster than official clients
- Download files from (protected) chats
- Forward messages with automatic fallback and message routing
- Upload files to Telegram
- Export messages/members/subscribers to JSON
## Preview
It reaches my proxy's speed limit, and the **speed depends on whether you are a premium**
{{< image src="img/preview.gif" >}}
## Sponsors

## Contributors
code
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
` + From.VisibleName
```
{{< /details >}}
{{< command >}}
tdl forward --from tdl-export.json --edit edit.txt
{{< /command >}}
## Dry Run
Print the progress without actually sending messages, which is useful for message routing debugging.
{{< command >}}
tdl forward --from tdl-export.json --dry-run
{{< /command >}}
## Silent
Send messages without notification.
{{< command >}}
tdl forward --from tdl-export.json --silent
{{< /command >}}
## No Grouped Detection
By default, tdl will detect grouped messages and forward them as an album.
You can disable this behavior by `--single` to forward it as a single message.
{{< command >}}
tdl forward --from tdl-export.json --single
{{< /command >}}
## Descending Order
Forward messages in descending order for each source.
{{< command >}}
tdl forward --from tdl-export.json --desc
{{< /command >}}
================================================
FILE: docs/content/en/guide/global-config.md
================================================
---
title: "Global Config"
weight: 10
---
# Global Config
Global config is some CLI flags that can be set in every command.
{{< hint info >}}
**Set Global Config EVERYTIME!**
Global config **does not mean** that the configuration will be persisted or only need to be set once in global settings, they will only take effect in the current command.
You need to set them in each command.
{{< /hint >}}
## `-n/--ns`
Each namespace represents a Telegram account. Default: `default`.
If you want to add another account, just add `-n YOUR_ACCOUNT_NAME` option to every command:
{{< command >}}
tdl -n iyear
{{< /command >}}
## `--proxy`
Set the proxy. Default: `""`.
Format: `protocol://username:password@host:port`
{{< command >}}
tdl --proxy socks5://localhost:1080
tdl --proxy http://localhost:8080
tdl --proxy https://localhost:8081
{{< /command >}}
## `--storage`
Set the storage. Default: `type=bolt,path=~/.tdl/data`
Format: `type=DRIVER,opt1=val1,opt2=val2,...`
Available drivers:
| Driver | Options | Description |
|:----------------:|:------------------------------:|---------------------------------------------------------------------------------------------------------------|
| `bolt` (Default) | `path=/path/to/data-directory` | Store data in separate database files. So you can use it in multiple processes(must be different namespaces). |
| `file` | `path=/path/to/data.json` | Store data in a single JSON file, which is useful for debugging. |
| `legacy` | `path=/path/to/data.kv` | **Deprecated.** Store data in a single database file. So you **can't** use it in multiple processes. |
| - | - | Wait for more drivers... |
{{< command >}}
tdl --storage type=bolt,path=/path/to/data-dir
{{< /command >}}
## `--ntp`
Set ntp server host. If it's empty, system time will be used. Default: `""`.
{{< command >}}
tdl --ntp pool.ntp.org
{{< /command >}}
## `--reconnect-timeout`
Set Telegram client reconnect timeout. Default: `2m`.
{{< hint info >}}
Set higher timeout or 0(INF) if your network is poor.
{{< /hint >}}
{{< command >}}
tdl --reconnect-timeout 1m30s
{{< /command >}}
## `--debug`
Enable debug level log. Default: `false`.
{{< command >}}
tdl --debug
{{< /command >}}
## `--pool`
Set the DC pool size of Telegram client. Default: `8`.
{{< hint info >}}
Set higher timeout or 0(INF) if you want faster speed.
{{< /hint >}}
{{< command >}}
tdl --pool 2
{{< /command >}}
## `--delay`
set the delay between each task. Default: `0s`.
{{< hint info >}}
Set higher delay time if you want to avoid Telegram's flood control.
{{< /hint >}}
{{< command >}}
tdl --delay 5s
{{< /command >}}
================================================
FILE: docs/content/en/guide/login.md
================================================
---
type: "docs"
title: "Login"
weight: 20
bookHref: "/getting-started/quick-start/#login"
---
# Login
================================================
FILE: docs/content/en/guide/migration.md
================================================
---
title: "Migration"
weight: 50
---
# Migration
Backup or recover your data
## Backup
Backup all namespace data to a file. Default: `code
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
` + MIME
```
{{< /details >}}
{{< command >}}
tdl up -p /path/to/file --caption caption.txt
{{< /command >}}
## Filters
Upload files with extension filters:
{{< hint warning >}}
The extension is only matched with the file name, not the MIME type. So it may not work as expected.
Whitelist and blacklist can not be used at the same time.
{{< /hint >}}
Whitelist: Only upload files with `.jpg` `.png` extension
{{< command >}}
tdl up -p /path/to/file -p /path/to/dir -i jpg,png
{{< /command >}}
Blacklist: Upload all files except `.mp4` `.flv` extension
{{< command >}}
tdl up -p /path/to/file -p /path/to/dir -e mp4 -e flv
{{< /command >}}
## Delete Local
Delete the uploaded file after uploading successfully:
{{< command >}}
tdl up -p /path/to/file --rm
{{< /command >}}
## Photo
Upload images as photos instead of documents:
{{< command >}}
tdl up -p /path/to/file --photo
{{< /command >}}
================================================
FILE: docs/content/en/more/_index.md
================================================
---
title: "More"
bookFlatSection: true
weight: 30
---
================================================
FILE: docs/content/en/more/cli/_index.md
================================================
---
title: "CLI"
weight: 10
bookHref: "/more/cli/tdl"
---
================================================
FILE: docs/content/en/more/data.md
================================================
---
title: "Data"
weight: 30
---
# Data
Your account information will be stored in the `~/.tdl` directory.
Log files will be stored in the `~/.tdl/log` directory.
================================================
FILE: docs/content/en/more/env.md
================================================
---
title: "Env"
weight: 20
---
# Env
{{< hint info >}}
The values of all environment variables have a lower priority than flags.
{{< /hint >}}
Avoid typing the same flag values repeatedly every time by setting environment variables.
| NAME | FLAG |
|:-----------------------:|:---------------------:|
| `TDL_NS` | `-n/--ns` |
| `TDL_PROXY` | `--proxy` |
| `TDL_STORAGE` | `--storage` |
| `TDL_DEBUG` | `--debug` |
| `TDL_SIZE` | `-s/--size` |
| `TDL_THREADS` | `-t/--threads` |
| `TDL_LIMIT` | `-l/--limit` |
| `TDL_POOL` | `--pool` |
| `TDL_NTP` | `--ntp` |
| `TDL_RECONNECT_TIMEOUT` | `--reconnect-timeout` |
| `TDL_TEMPLATE` | dl `--template` |
{{< hint warning >}}
- `TDL_STORAGE` format in env is different from that in flags: `{"type": "bolt", "path": "/path/to/data-dir"}` (JSON object).
{{< /hint >}}
================================================
FILE: docs/content/en/more/troubleshooting.md
================================================
---
title: "Troubleshooting"
weight: 40
---
# Troubleshooting
## Best Practices
How to minimize the risk of blocking?
- Login with the official client session.
- Use the default download and upload options as possible. Do not set too large `threads` and `size`.
- Do not use the same account to login on multiple devices at the same time.
- Don't download or upload too many files at once.
- Become a Telegram premium user. 😅
## FAQ
#### Q: Why no response after entering the command? And why there is `msg_id too high` in the log?
**A:** Check if you need to use a proxy (use `proxy` flag); Check if your system's local time is correct (use `ntp` flag
or calibrate system time)
If that doesn't work, run again with `--debug` flag. Then file a new issue and paste your log in the issue.
#### Q: Desktop client stop working after using tdl?
**A:** If your desktop client can't receive messages, load chats, or send messages, you may encounter session conflicts.
You can try re-login with `tdl login` and **select YES for logout**, which will delete the session files to separate
sessions.
#### Q: How to migrate session to another device?
**A:** You can use the `tdl backup` and `tdl recover` commands to export and import sessions.
See [Migration](/guide/migration) for more details.
#### Q: Is this a form of abuse?
**A:** No. The download and upload speed is limited by the server side. Since the speed of official clients usually does
not
reach the account limit, this tool was developed to download files at the highest possible speed.
#### Q: Will this result in a ban?
**A:** I am not sure. All operations do not involve dangerous actions such as actively sending messages to other people.
But
it's safer to use a long-term account.
================================================
FILE: docs/content/en/reference/_index.md
================================================
---
bookHidden: true
---
================================================
FILE: docs/content/en/reference/expr.md
================================================
---
title: "Expr Guide"
bookHidden: true
---
# Expr Guide
Expr is powered by [expr](https://github.com/antonmedv/expr), which is a simple, lightweight, yet powerful expression
engine.
Expression engine docs: https://expr.medv.io/docs/Language-Definition
It's powerful but may be a little hard to new users. So feel free to file an issue if you have any questions about the
expression engine.
================================================
FILE: docs/content/en/snippets/_index.md
================================================
---
bookHidden: true
---
================================================
FILE: docs/content/en/snippets/chat.md
================================================
---
---
{{< details title="CHAT Examples" open=false >}}
#### Available Values:
- `@iyear` (Username)
- `iyear` (Username without `@`)
- `123456789` (ID)
- `https://t.me/iyear` (Public Link)
- `+1 123456789` (Phone)
#### How to Get Chat ID on Telegram Desktop:
- `Settings` → `Advanced` → `Experimental settings` → `Show Peer IDs in Profile`
{{< /details >}}
================================================
FILE: docs/content/en/snippets/link.md
================================================
---
---
{{< details title="Message Link Examples" open=false >}}
- `https://t.me/telegram/193`
- `https://t.me/c/1697797156/151`
- `https://t.me/iFreeKnow/45662/55005`
- `https://t.me/c/1492447836/251015/251021`
- `https://t.me/opencfdchannel/4434?comment=360409`
- `https://t.me/myhostloc/1485524?thread=1485523`
- `...` (File a new issue if you find a new link format)
{{< /details >}}
================================================
FILE: docs/content/zh/_index.md
================================================
---
title: 介绍
---
# tdl





{{< image src="img/logo.png" align="right" height="310" width="310">}}
📥 Telegram Downloader, but more than a downloader
## 特性
- 单文件启动
- 低资源占用
- 吃满你的带宽
- 比官方客户端更快
- 支持从受保护的会话中下载文件
- 具有自动回退和消息路由的转发功能
- 支持上传文件至 Telegram
- 导出历史消息/成员/订阅者数据至 JSON 文件
## 预览
预览中的速度已经达到了代理的限制,同时**速度取决于你是否是付费用户**
{{< image src="img/preview.gif" >}}
## 赞助者

## 贡献者
代码
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
` + From.VisibleName
```
{{< /details >}}
{{< command >}}
tdl forward --from tdl-export.json --edit edit.txt
{{< /command >}}
## 试运行
只打印进度而不实际发送消息,可以用于调试消息路由的效果。
{{< command >}}
tdl forward --from tdl-export.json --dry-run
{{< /command >}}
## 静默发送
发送消息而不通知其他成员。
{{< command >}}
tdl forward --from tdl-export.json --silent
{{< /command >}}
## 取消分组检测
默认情况下,tdl 将自动探测到分组消息并将它们转发为合并的消息。
你可以通过 `--single` 禁用此行为,将其作为单个消息转发。
{{< command >}}
tdl forward --from tdl-export.json --single
{{< /command >}}
## 反序
对每个来源的消息进行反序转发。
{{< command >}}
tdl forward --from tdl-export.json --desc
{{< /command >}}
================================================
FILE: docs/content/zh/guide/global-config.md
================================================
---
title: "全局设置"
weight: 10
---
# 全局配置
全局配置是可以在每个命令中设置的选项。
{{< hint info >}}
**每次都设置全局配置!**
全局配置**不代表**配置会被持久化或者只需要在全局设置一次,它们只会在当前命令中生效。
你需要在每一个命令中设置它们。
{{< /hint >}}
## `-n/--ns`
每个命名空间代表一个 Telegram 帐号。默认值:`default`。
例如你想新增一个其他账户,为所有命令都添加 `-n YOUR_ACCOUNT_NAME` 选项即可:
{{< command >}}
tdl -n iyear
{{< /command >}}
## `--proxy`
设置代理。默认值:`""`。
格式:`protocol://username:password@host:port`
{{< command >}}
tdl --proxy socks5://localhost:1080
tdl --proxy http://localhost:8080
tdl --proxy https://localhost:8081
{{< /command >}}
## `--storage`
设置存储。默认值:`type=bolt,path=~/.tdl/data`
格式: `type=驱动,opt1=val1,opt2=val2,...`
可用的驱动:
| 驱动名 | 选项 | 描述 |
|:----------:|:------------------------------:|---------------------------------------------|
| `bolt`(默认) | `path=/path/to/data-directory` | 将数据存储在单独的数据库文件中,因此您可以在多个进程中使用(但必须是不同的命名空间)。 |
| `file` | `path=/path/to/data.json` | 将数据存储在单个 JSON 文件中,通常用于调试。 |
| `legacy` | `path=/path/to/data.kv` | **已弃用。** 将数据存储在单个数据库文件中,因此你**不能**在多个进程中使用它。 |
| - | - | 等待更多驱动... |
{{< command >}}
tdl --storage type=bolt,path=/path/to/data-dir
{{< /command >}}
## `--ntp`
设置 NTP 服务器。如果为空,将使用系统时间。默认值:`""`。
{{< command >}}
tdl --ntp pool.ntp.org
{{< /command >}}
## `--reconnect-timeout`
设置 Telegram 连接的重连超时。默认值:`2m`。
{{< hint info >}}
如果您的网络不稳定,请将超时设置为更长时间或0(无限)。
{{< /hint >}}
{{< command >}}
tdl --reconnect-timeout 1m30s
{{< /command >}}
## `--debug`
启用调试级别日志。默认值:`false`。
{{< command >}}
tdl --debug
{{< /command >}}
## `--pool`
设置 Telegram 客户端的连接池大小。默认值:`8`。
{{< hint info >}}
如果你想要更快的速度,请将连接池设置的更大或者0(无限)。
{{< /hint >}}
{{< command >}}
tdl --pool 2
{{< /command >}}
## `--delay`
设置每个任务之间的延迟。默认值:`0s`。
{{< hint info >}}
如果你想避免因为短时间内产生大量请求被限流,请设置更长的延迟时间。
{{< /hint >}}
{{< command >}}
tdl --delay 5s
{{< /command >}}
================================================
FILE: docs/content/zh/guide/login.md
================================================
---
type: "docs"
title: "登录"
weight: 20
bookHref: "/zh/getting-started/quick-start/#login"
---
# Login
================================================
FILE: docs/content/zh/guide/migration.md
================================================
---
title: "迁移"
weight: 50
---
# 迁移
备份或恢复您的数据
## 备份
将您的数据备份到文件中。默认值:`code
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
` + MIME
```
{{< /details >}}
{{< command >}}
tdl up -p /path/to/file --caption caption.txt
{{< /command >}}
## 过滤器
使用扩展名过滤器上传文件:
{{< hint warning >}}
扩展名仅与文件名匹配,而不是 MIME 类型。因此,这可能不会按预期工作。
白名单和黑名单不能同时使用。
{{< /hint >}}
白名单:只上传扩展名为 `.jpg` `.png` 的文件
{{< command >}}
tdl up -p /path/to/file -p /path/to/dir -i jpg,png
{{< /command >}}
黑名单:上传除了扩展名为 `.mp4` `.flv` 的所有文件
{{< command >}}
tdl up -p /path/to/file -p /path/to/dir -e mp4 -e flv
{{< /command >}}
## 自动删除
删除已上传成功的文件:
{{< command >}}
tdl up -p /path/to/file --rm
{{< /command >}}
## 照片
将图像作为照片而不是文件上传:
{{< command >}}
tdl up -p /path/to/file --photo
{{< /command >}}
================================================
FILE: docs/content/zh/more/_index.md
================================================
---
title: "更多"
bookFlatSection: true
weight: 30
---
================================================
FILE: docs/content/zh/more/cli/_index.md
================================================
---
title: "命令行"
weight: 10
bookHref: "/more/cli/tdl"
---
================================================
FILE: docs/content/zh/more/data.md
================================================
---
title: "数据"
weight: 30
---
# 数据
您的帐户信息将存储在 `~/.tdl` 目录中。
日志文件将存储在 `~/.tdl/log` 目录中。
================================================
FILE: docs/content/zh/more/env.md
================================================
---
title: "环境变量"
weight: 20
---
# 环境变量
{{< hint info >}}
所有环境变量的值优先级低于命令行选项。
{{< /hint >}}
通过设置环境变量,避免在每次重复输入相同的命令行选项。
| 环境变量 | 对应选项 |
|:-----------------------:|:---------------------:|
| `TDL_NS` | `-n/--ns` |
| `TDL_PROXY` | `--proxy` |
| `TDL_STORAGE` | `--storage` |
| `TDL_DEBUG` | `--debug` |
| `TDL_SIZE` | `-s/--size` |
| `TDL_THREADS` | `-t/--threads` |
| `TDL_LIMIT` | `-l/--limit` |
| `TDL_POOL` | `--pool` |
| `TDL_NTP` | `--ntp` |
| `TDL_RECONNECT_TIMEOUT` | `--reconnect-timeout` |
| `TDL_TEMPLATE` | dl `--template` |
{{< hint warning >}}
- `TDL_STORAGE` 环境变量的格式与命令行选项不同:`{"type": "bolt", "path": "/path/to/data-dir"}` (JSON 对象)。
{{< /hint >}}
================================================
FILE: docs/content/zh/more/troubleshooting.md
================================================
---
title: "疑难解答"
weight: 40
---
# 疑难解答
## 最佳实践
如何减小封号的风险?
- 使用官方客户端会话登录。
- 尽可能使用默认的下载和上传选项。不要设置过大的 `threads` 和 `size`。
- 不要同时在多台设备上使用相同的帐户登录。
- 不要同时下载或上传太多文件。
- 成为 Telegram 大会员。😅
## 常见问题
#### Q: 输入命令后为什么没有响应?日志中为什么出现 `msg_id too high`?
**A:** 检查是否需要使用代理(使用 `--proxy` 选项);检查您系统的本地时间是否正确(使用 `--ntp` 选项或校准系统时间)
如果仍然无法解决问题,请使用 `--debug` 标志重新运行。然后创建一个新的 ISSUE 并将日志粘贴到问题中。
#### Q: 使用 tdl 后,桌面客户端停止工作怎么办?
**A:** 如果您的桌面客户端无法接收消息、加载聊天或发送消息,可能遇到了会话冲突。
您可以尝试使用 `tdl login` 重新登录,并**选择 YES 以退出桌面客户端登录**,这将删除客户端会话文件以分离会话。
#### Q: 如何将会话迁移到另一台设备?
**A:** 您可以使用 `tdl backup` 和 `tdl recover` 命令导出和导入会话。详细信息请参见 [迁移](/zh/guide/migration)。
#### Q: 这算滥用吗?
**A:** 不是。下载和上传速度受服务器端限制。由于官方客户端的速度通常不会达到帐户限制,因此开发了此工具,以尽可能高的速度下载文件。
#### Q: 这会导致封禁吗?
**A:** 我不确定。所有操作都不涉及向其他人主动发送消息等高风险行为。
================================================
FILE: docs/content/zh/reference/_index.md
================================================
---
bookHidden: true
---
================================================
FILE: docs/content/zh/reference/expr.md
================================================
---
title: "表达式指南"
bookHidden: true
---
# 表达式指南
表达式由 [expr](https://github.com/antonmedv/expr) 引擎提供支持,它是一个简单、轻量但功能强大的表达式引擎。
表达式引擎文档:https://expr.medv.io/docs/Language-Definition
它功能强大,但对于新用户来说可能有些难以理解。如果您对表达式引擎有任何疑问,请随时提出 ISSUE。
================================================
FILE: docs/content/zh/snippets/_index.md
================================================
---
bookHidden: true
---
================================================
FILE: docs/content/zh/snippets/chat.md
================================================
---
---
{{< details title="CHAT 示例" open=false >}}
#### 可用值:
- `@iyear` (用户名)
- `iyear` (无前缀 `@` 的用户名)
- `123456789`(ID)
- `https://t.me/iyear` (公开链接)
- `+1 123456789`(电话号码)
#### 如何在 Telegram 桌面端获取聊天 ID:
- `设置` → `高级` → `实验性设置` → `在资料中显示对话 ID`
{{< /details >}}
================================================
FILE: docs/content/zh/snippets/link.md
================================================
---
---
{{< details title="消息链接示例" open=false >}}
- `https://t.me/telegram/193`
- `https://t.me/c/1697797156/151`
- `https://t.me/iFreeKnow/45662/55005`
- `https://t.me/c/1492447836/251015/251021`
- `https://t.me/opencfdchannel/4434?comment=360409`
- `https://t.me/myhostloc/1485524?thread=1485523`
- `...`(如果发现新的链接格式,请提交新的 Issue)
{{< /details >}}
================================================
FILE: docs/go.mod
================================================
module github.com/iyear/tdl/docs
go 1.25.8
require github.com/alex-shpak/hugo-book v0.0.0-20230808113920-3f1bcccbfb24 // indirect
================================================
FILE: docs/go.sum
================================================
github.com/alex-shpak/hugo-book v0.0.0-20230808113920-3f1bcccbfb24 h1:8NjMYBSFTtBLeT1VmpZAZznPOt1OH8aNCnE86sL4p4k=
github.com/alex-shpak/hugo-book v0.0.0-20230808113920-3f1bcccbfb24/go.mod h1:L4NMyzbn15fpLIpmmtDg9ZFFyTZzw87/lk7M2bMQ7ds=
================================================
FILE: docs/hugo.yaml
================================================
baseURL: "https://example.com/"
title: tdl
enableGitInfo: true
canonifyURLs: true
module:
imports:
- path: github.com/alex-shpak/hugo-book
# Needed for mermaid/katex shortcodes
markup:
highlight:
style: onedark
goldmark:
renderer:
unsafe: true
tableOfContents:
startLevel: 1
params:
BookTheme: auto
BookRepo: https://github.com/iyear/tdl
BookSection: "*"
BookSearch: true # container will be mounted by docsearch
BookEditPath: edit/master/docs
BookCommitPath: commit
BookDateFormat: 2006/01/02
BookTranslatedOnly: true
languages:
en:
languageName: English
contentDir: content/en
weight: 1
menu:
before:
- name: "🔗 GitHub"
url: "https://github.com/iyear/tdl"
weight: 10
- name: "👨💻 Author"
url: "https://github.com/iyear"
weight: 20
- name: "🌟 Donate"
url: "https://www.patreon.com/iyear"
weight: 30
zh:
languageName: 简体中文
contentDir: content/zh
weight: 2
menu:
before:
- name: "🔗 项目主页"
url: "https://github.com/iyear/tdl"
weight: 10
- name: "👨💻 作者"
url: "https://github.com/iyear"
weight: 20
- name: "🌟 捐赠"
url: "https://afdian.net/a/iyear"
weight: 30
================================================
FILE: docs/layouts/partials/docs/inject/footer.html
================================================
================================================
FILE: docs/layouts/partials/docs/inject/head.html
================================================
================================================
FILE: docs/layouts/shortcodes/command.html
================================================
{{- $input := trim .Inner " \t\r\n" -}}
{{- $lines := split $input "\n" -}}
{{- $slash := false -}}
{{- range $line := $lines -}}
{{- $line = trim $line " \t\r\n" -}}
{{- if ne $line "" -}}
{{- if not $slash -}}
{{ $line }}
{{- else -}}
{{ $line }}
{{- end -}}
{{- $slash = hasSuffix $line "\\" -}}
{{- end -}}
{{- end -}}
================================================
FILE: docs/layouts/shortcodes/image.html
================================================
{{ $file := .Get "src" }}
{{ $height := .Get "height" }}
{{ $width := .Get "width" }}
{{ $align := .Get "align" }}
{{ with resources.Get $file }}