Showing preview only (568K chars total). Download the full file or copy to clipboard to get everything.
Repository: iyear/tdl
Branch: master
Commit: 7c1b61b0a6f1
Files: 234
Total size: 517.4 KB
Directory structure:
gitextract_qifrln_6/
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yaml
│ │ └── feature_request.yml
│ ├── dependabot.yml
│ └── workflows/
│ ├── dependabot-fix.yml
│ ├── docker.yml
│ ├── docs.yml
│ ├── master.yml
│ └── release.yml
├── .gitignore
├── .golangci.yaml
├── .goreleaser.yaml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── README_zh.md
├── app/
│ ├── chat/
│ │ ├── export.go
│ │ ├── export_enum.go
│ │ ├── ls.go
│ │ ├── ls_enum.go
│ │ └── users.go
│ ├── dl/
│ │ ├── dl.go
│ │ ├── elem.go
│ │ ├── iter.go
│ │ ├── iter_test.go
│ │ ├── progress.go
│ │ ├── serve.go
│ │ └── serve.go.tmpl
│ ├── extension/
│ │ └── extension.go
│ ├── forward/
│ │ ├── elem.go
│ │ ├── forward.go
│ │ ├── iter.go
│ │ └── progress.go
│ ├── internal/
│ │ └── tctx/
│ │ └── tctx.go
│ ├── login/
│ │ ├── code.go
│ │ ├── desktop.go
│ │ ├── login.go
│ │ ├── login_enum.go
│ │ └── qr.go
│ ├── migrate/
│ │ ├── backup.go
│ │ ├── migrate.go
│ │ └── recover.go
│ └── up/
│ ├── elem.go
│ ├── iter.go
│ ├── progress.go
│ ├── up.go
│ └── walk.go
├── cmd/
│ ├── chat.go
│ ├── dl.go
│ ├── extension.go
│ ├── forward.go
│ ├── gen.go
│ ├── login.go
│ ├── migrate.go
│ ├── root.go
│ ├── up.go
│ ├── version.go
│ └── version.tmpl
├── core/
│ ├── dcpool/
│ │ ├── dcpool.go
│ │ └── middlewares.go
│ ├── downloader/
│ │ ├── downloader.go
│ │ ├── iter.go
│ │ └── progress.go
│ ├── forwarder/
│ │ ├── clone.go
│ │ ├── forwarder.go
│ │ ├── forwarder_enum.go
│ │ ├── iter.go
│ │ └── progress.go
│ ├── go.mod
│ ├── go.sum
│ ├── logctx/
│ │ └── logctx.go
│ ├── middlewares/
│ │ ├── recovery/
│ │ │ └── recovery.go
│ │ ├── retry/
│ │ │ └── retry.go
│ │ └── takeout/
│ │ ├── middleware.go
│ │ └── takeout.go
│ ├── storage/
│ │ ├── keygen/
│ │ │ └── keygen.go
│ │ ├── peers.go
│ │ ├── session.go
│ │ ├── state.go
│ │ └── storage.go
│ ├── tclient/
│ │ └── tclient.go
│ ├── tmedia/
│ │ ├── convert.go
│ │ ├── document.go
│ │ ├── media.go
│ │ └── photo.go
│ ├── uploader/
│ │ ├── iter.go
│ │ ├── progress.go
│ │ └── uploader.go
│ └── util/
│ ├── fsutil/
│ │ └── fsutil.go
│ ├── logutil/
│ │ └── logutil.go
│ ├── mediautil/
│ │ └── mediautil.go
│ ├── netutil/
│ │ └── netutil.go
│ └── tutil/
│ ├── device.go
│ └── tutil.go
├── docs/
│ ├── assets/
│ │ └── _custom.scss
│ ├── content/
│ │ ├── en/
│ │ │ ├── _index.md
│ │ │ ├── getting-started/
│ │ │ │ ├── _index.md
│ │ │ │ ├── installation.md
│ │ │ │ ├── quick-start.md
│ │ │ │ └── shell-completion.md
│ │ │ ├── guide/
│ │ │ │ ├── _index.md
│ │ │ │ ├── download.md
│ │ │ │ ├── extensions.md
│ │ │ │ ├── forward.md
│ │ │ │ ├── global-config.md
│ │ │ │ ├── login.md
│ │ │ │ ├── migration.md
│ │ │ │ ├── template.md
│ │ │ │ ├── tools/
│ │ │ │ │ ├── _index.md
│ │ │ │ │ ├── export-members.md
│ │ │ │ │ ├── export-messages.md
│ │ │ │ │ └── list-chats.md
│ │ │ │ └── upload.md
│ │ │ ├── more/
│ │ │ │ ├── _index.md
│ │ │ │ ├── cli/
│ │ │ │ │ └── _index.md
│ │ │ │ ├── data.md
│ │ │ │ ├── env.md
│ │ │ │ └── troubleshooting.md
│ │ │ ├── reference/
│ │ │ │ ├── _index.md
│ │ │ │ └── expr.md
│ │ │ └── snippets/
│ │ │ ├── _index.md
│ │ │ ├── chat.md
│ │ │ └── link.md
│ │ └── zh/
│ │ ├── _index.md
│ │ ├── getting-started/
│ │ │ ├── _index.md
│ │ │ ├── installation.md
│ │ │ ├── quick-start.md
│ │ │ └── shell-completion.md
│ │ ├── guide/
│ │ │ ├── _index.md
│ │ │ ├── download.md
│ │ │ ├── extensions.md
│ │ │ ├── forward.md
│ │ │ ├── global-config.md
│ │ │ ├── login.md
│ │ │ ├── migration.md
│ │ │ ├── template.md
│ │ │ ├── tools/
│ │ │ │ ├── _index.md
│ │ │ │ ├── export-members.md
│ │ │ │ ├── export-messages.md
│ │ │ │ └── list-chats.md
│ │ │ └── upload.md
│ │ ├── more/
│ │ │ ├── _index.md
│ │ │ ├── cli/
│ │ │ │ └── _index.md
│ │ │ ├── data.md
│ │ │ ├── env.md
│ │ │ └── troubleshooting.md
│ │ ├── reference/
│ │ │ ├── _index.md
│ │ │ └── expr.md
│ │ └── snippets/
│ │ ├── _index.md
│ │ ├── chat.md
│ │ └── link.md
│ ├── go.mod
│ ├── go.sum
│ ├── hugo.yaml
│ ├── layouts/
│ │ ├── partials/
│ │ │ └── docs/
│ │ │ └── inject/
│ │ │ ├── footer.html
│ │ │ └── head.html
│ │ └── shortcodes/
│ │ ├── command.html
│ │ ├── image.html
│ │ └── include.html
│ └── resources/
│ └── _gen/
│ └── assets/
│ └── scss/
│ ├── book.scss_e129fe35b8d0a70789c8a08429469073.content
│ └── book.scss_e129fe35b8d0a70789c8a08429469073.json
├── extension/
│ ├── extension.go
│ ├── go.mod
│ └── go.sum
├── go.mod
├── go.sum
├── go.work
├── go.work.sum
├── hack/
│ ├── lib.sh
│ └── release_mod.sh
├── main.go
├── pkg/
│ ├── clock/
│ │ └── clock.go
│ ├── consts/
│ │ ├── consts.go
│ │ ├── flag.go
│ │ ├── path.go
│ │ └── version.go
│ ├── extensions/
│ │ ├── extensions.go
│ │ ├── extensions_enum.go
│ │ ├── extensions_test.go
│ │ ├── github.go
│ │ ├── local.go
│ │ ├── local_test.go
│ │ └── manager.go
│ ├── filterMap/
│ │ └── filterMap.go
│ ├── key/
│ │ └── key.go
│ ├── kv/
│ │ ├── bolt.go
│ │ ├── file.go
│ │ ├── kv.go
│ │ ├── kv_enum.go
│ │ ├── kv_test.go
│ │ └── legacy.go
│ ├── prog/
│ │ ├── prog.go
│ │ └── tracker.go
│ ├── ps/
│ │ └── ps.go
│ ├── tclient/
│ │ ├── app.go
│ │ └── tclient.go
│ ├── tdesktop/
│ │ ├── .s
│ │ └── tdesktop.go
│ ├── texpr/
│ │ ├── env.go
│ │ ├── env_test.go
│ │ ├── expr.go
│ │ ├── fields.go
│ │ └── fields_test.go
│ ├── tmessage/
│ │ ├── files.go
│ │ ├── tmessage.go
│ │ └── urls.go
│ ├── tpath/
│ │ ├── tpath.go
│ │ ├── tpath_darwin.go
│ │ ├── tpath_linux.go
│ │ ├── tpath_other.go
│ │ └── tpath_windows.go
│ ├── tplfunc/
│ │ ├── date.go
│ │ ├── date_test.go
│ │ ├── func.go
│ │ ├── math.go
│ │ ├── math_test.go
│ │ ├── string.go
│ │ └── string_test.go
│ ├── utils/
│ │ ├── byte.go
│ │ └── cmd.go
│ └── validator/
│ └── validator.go
├── scripts/
│ ├── install.ps1
│ └── install.sh
└── test/
├── archive_test.go
├── chat_ls_test.go
├── chat_users_test.go
├── download_test.go
├── suite_test.go
├── testserver/
│ ├── public_key.pem
│ └── testserver.go
└── upload_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.{go,mod}]
indent_style = tab
[Makefile]
indent_style = tab
[*.md]
max_line_length = off
trim_trailing_whitespace = false
================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
*.{png,jpg,jpeg,gif,webp,woff,woff2} binary
# https://joshuatz.com/posts/2019/how-to-get-github-to-recognize-a-pure-markdown-repo/
*.md linguist-vendored=false
*.md linguist-generated=false
*.md linguist-documentation=false
*.md linguist-detectable=true
================================================
FILE: .github/CODEOWNERS
================================================
* @iyear
*.go @XMLHexagram
*.md @XMLHexagram
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yaml
================================================
name: Bug Report
description: Create a report to help us improve
title: "[Bug] Please complete title/请完善标题"
labels: ["bug"]
assignees:
- iyear
body:
- type: markdown
attributes:
value: |
> [!IMPORTANT]
> Make sure to browse the opened and closed issues before submit your issue.
>
> 对于中文用户,请使用英文编写标题和内容(可以选择使用机器翻译)
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is
placeholder: It always crashes when I do [...]
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: To Reproduce
description: Steps to reproduce the behavior
placeholder: |
1. Run '...'
2. Click on '....'
3. See error
validations:
required: true
- type: textarea
id: expectation
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
placeholder: |
It should do [...]
validations:
required: true
- type: textarea
id: version
attributes:
label: Version
description: |
```console
$ tdl version
// output
```
placeholder: |
Version: 0.14.1
Commit: 3021de5
Date: 2024-01-05T16:27:43Z
go1.19.13 windows/amd64
validations:
required: true
- type: dropdown
id: os
attributes:
label: Which OS are you running tdl on?
multiple: false
options:
- Windows
- macOS
- Linux
- Other
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context about the problem here
placeholder: |
Logs, screenshots, etc.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature Request
description: Suggest an idea for tdl
title: "[Feat] Please complete title/请完善标题"
labels: ["enhancement"]
assignees:
- iyear
body:
- type: markdown
attributes:
value: |
> [!IMPORTANT]
> Make sure to browse the opened and closed issues before submit your issue.
>
> 对于中文用户,请使用英文编写标题和内容(可以选择使用机器翻译)
- type: textarea
id: proposal
attributes:
label: Proposal
description: Write your feature request in the form of a proposal to be considered for implementation.
validations:
required: true
- type: textarea
id: background
attributes:
label: Background
description: Describe the background problem or need that led to this feature request.
validations:
required: true
- type: textarea
id: workarounds
attributes:
label: Workarounds
description: Are there any current workarounds that you're using that others in similar positions should know about?
validations:
required: true
================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gomod" # See documentation for possible values
directories:
- "/"
- "/core"
- "/extension"
schedule:
interval: "daily"
assignees:
- "iyear"
open-pull-requests-limit: 99
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
assignees:
- "iyear"
open-pull-requests-limit: 99
================================================
FILE: .github/workflows/dependabot-fix.yml
================================================
name: dependabot-fix
on:
pull_request:
branches:
- dependabot/go_modules/**
push:
branches:
- dependabot/go_modules/**
jobs:
tidy:
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Golang env
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- uses: evantorrie/mott-the-tidier@v1-beta
with:
# mod tidy all modules except docs(hugo module)
gomods: |
**/go.mod
-docs/go.mod
- uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "chore(deps): tidy modules"
================================================
FILE: .github/workflows/docker.yml
================================================
name: docker
on:
workflow_dispatch:
inputs:
ref:
description: 'Ref to checkout'
required: true
default: 'master'
push:
tags:
- 'v*'
jobs:
docker:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
with:
context: git
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}
tags: |
type=match,pattern=\d+.\d+.\d+
type=ref,event=branch
type=ref,event=pr
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
uses: docker/login-action@v4
with:
username: ${{ github.repository_owner }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.RELEASE_TOKEN }}
- name: Use latest Dockerfile if workflow_dispatch
if: ${{ github.event_name == 'workflow_dispatch' }}
run: |
curl -s https://raw.githubusercontent.com/iyear/tdl/master/Dockerfile > Dockerfile
- name: Extract Dockerfile args
id: args
run: |
echo "commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
echo "commit_date=$(git show -s --format=%cI)" >> "$GITHUB_OUTPUT"
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ steps.meta.outputs.version }}
COMMIT=${{ steps.args.outputs.commit }}
COMMIT_DATE=${{ steps.args.outputs.commit_date }}
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/arm/v6,linux/riscv64
push: true
provenance: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
================================================
FILE: .github/workflows/docs.yml
================================================
name: deploy docs
on:
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-22.04
env:
HUGO_VERSION: 0.119.0 # also update cloudflare pages env variable if changed
steps:
- name: Install Hugo CLI
run: |
wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
&& sudo dpkg -i ${{ runner.temp }}/hugo.deb
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
- name: Setup Golang env
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Generate CLI docs
run: go run main.go gen doc -d docs/content/en/more/cli
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5
- name: Build with Hugo
env:
HUGO_ENVIRONMENT: production
HUGO_ENV: production
run: |
cd docs
hugo \
--gc \
--minify \
--baseURL "${{ steps.pages.outputs.base_url }}/"
- name: Copy install scripts
run: |
cp -r scripts/install.* docs/public
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
with:
path: ./docs/public
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-22.04
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .github/workflows/master.yml
================================================
name: master builder
on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
branches: [master]
push:
branches: [master]
paths-ignore:
- "docs/**"
permissions:
contents: read
pull-requests: read
jobs:
lint:
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
directory:
- .
- core
- extension
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Golang env
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: false # conflict with golangci-lint cache
- name: lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.6.0
working-directory: ${{ matrix.directory }}
unit-test:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Golang env
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Build
run: go build
- name: Unit Test
run: go test -v $(go list ./... | grep -v /test) # skip e2e test
e2e-test:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Golang env
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Install Ginkgo
run: go install github.com/onsi/ginkgo/v2/ginkgo
- name: Setup Teamgram Env
run: |
git clone https://github.com/iyear/teamgram-server.git
cd teamgram-server
git checkout 10151bb92555aa1bedcba9f8f24b0e7deac22dee
sudo docker compose -f ./docker-compose-env.yaml up -d --quiet-pull
sudo docker compose up -d --quiet-pull
- name: Build
run: go build
- name: E2E Test
run: ginkgo -v -r ./test
================================================
FILE: .github/workflows/release.yml
================================================
name: release
on:
workflow_dispatch:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
homebrew:
runs-on: ubuntu-22.04
steps:
- name: Bump Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@v7
with:
token: ${{ secrets.RELEASE_TOKEN }}
formula: telegram-downloader
goreleaser:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Fetch all tags
run: git fetch --force --tags
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Get previous tag
run: echo "PREV_TAG=$(git describe --tags --match "v*" --abbrev=0 HEAD^)" >> $GITHUB_ENV
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: v1.18.2
args: release --rm-dist --timeout 1h
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GORELEASER_PREVIOUS_TAG: ${{ env.PREV_TAG }}
================================================
FILE: .gitignore
================================================
.idea
log
.tdl
.vscode
downloads
*.exe
tdl
.hugo_build.lock
.DS_Store
================================================
FILE: .golangci.yaml
================================================
version: "2"
linters:
default: none
enable:
- exhaustive
- goconst
- govet
- ineffassign
- misspell
- nakedret
- staticcheck
- unconvert
- unused
- usestdlibvars
settings:
exhaustive:
default-signifies-exhaustive: true
nakedret:
max-func-lines: 0
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofumpt
settings:
gci:
sections:
- standard
- default
- prefix(github.com/iyear/tdl)
- dot
custom-order: true
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
================================================
FILE: .goreleaser.yaml
================================================
project_name: tdl
dist: .tdl/dist
env:
- GO111MODULE=on
builds:
- env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -s -w
- -X github.com/iyear/tdl/pkg/consts.Version={{ .Version }}
- -X github.com/iyear/tdl/pkg/consts.Commit={{ .ShortCommit }}
- -X github.com/iyear/tdl/pkg/consts.CommitDate={{ .CommitDate }}
mod_timestamp: '{{ .CommitTimestamp }}'
goos:
- linux
- windows
- darwin
goarch:
- 386
- amd64
- arm
- arm64
- riscv64
- loong64
goarm:
- 5
- 6
- 7
checksum:
name_template: '{{ .ProjectName }}_checksums.txt'
archives:
- name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
replacements:
darwin: MacOS
linux: Linux
windows: Windows
386: 32bit
amd64: 64bit
format_overrides:
- goos: windows
format: zip
files:
- README*.md
- LICENSE
changelog:
use: github
sort: asc
groups:
- title: New Features
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: 'Bug fixes'
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: 'Documentation updates'
regexp: "^.*docs[(\\w)]*:+.*$"
order: 2
- title: 'Refactoring'
regexp: "^.*refactor[(\\w)]*:+.*$"
order: 3
- title: Others
order: 4
release:
draft: true
================================================
FILE: Dockerfile
================================================
# https://www.docker.com/blog/faster-multi-platform-builds-dockerfile-cross-compilation-guide/
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
ARG VERSION="dev"
ARG COMMIT="unknown"
ARG COMMIT_DATE="unknown"
WORKDIR /
COPY . .
ARG TARGETOS
ARG TARGETARCH
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg \
GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -trimpath \
-ldflags "-s -w \
-X github.com/iyear/tdl/pkg/consts.Version=${VERSION} \
-X github.com/iyear/tdl/pkg/consts.Commit=${COMMIT} \
-X github.com/iyear/tdl/pkg/consts.CommitDate=${COMMIT_DATE}" \
-o tdl
FROM alpine:latest
RUN apk add --no-cache ca-certificates
COPY --from=builder /tdl /usr/bin/tdl
ENTRYPOINT ["tdl"]
================================================
FILE: LICENSE
================================================
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
================================================
FILE: Makefile
================================================
.PHONY: build
build:
goreleaser build --rm-dist --single-target --snapshot
@echo "go to '.tdl/dist' directory to see the package!"
.PHONY: packaging
packaging:
goreleaser release --skip-publish --auto-snapshot --rm-dist
@echo "go to '.tdl/dist' directory to see the packages!"
================================================
FILE: README.md
================================================
# tdl
<img align="right" src="docs/assets/img/logo.png" height="280" alt="">
> 📥 Telegram Downloader, but more than a downloader
English | <a href="README_zh.md">简体中文</a>
<p>
<img src="https://img.shields.io/github/go-mod/go-version/iyear/tdl?style=flat-square" alt="">
<img src="https://img.shields.io/github/license/iyear/tdl?style=flat-square" alt="">
<img src="https://img.shields.io/github/actions/workflow/status/iyear/tdl/master.yml?branch=master&style=flat-square" alt="">
<img src="https://img.shields.io/github/v/release/iyear/tdl?color=red&style=flat-square" alt="">
<img src="https://img.shields.io/github/downloads/iyear/tdl/total?style=flat-square" alt="">
</p>
#### 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**

## Documentation
Please refer to the [documentation](https://docs.iyear.me/tdl/).
## Sponsors

## Contributors
<a href="https://github.com/iyear/tdl/graphs/contributors">
<img src="https://contrib.rocks/image?repo=iyear/tdl&max=750&columns=20" alt="contributors"/>
</a>
## LICENSE
AGPL-3.0 License
================================================
FILE: README_zh.md
================================================
> [!IMPORTANT]
> 中文文档可能落后于英文文档,如果有问题请先查看英文文档。
> 请使用英文发起新的 Issue, 以便于追踪和搜索
# tdl
<img align="right" src="docs/assets/img/logo.png" height="280" alt="">
> 📥 Telegram Downloader, but more than a downloader
<a href="README.md">English</a> | 简体中文
<p>
<img src="https://img.shields.io/github/go-mod/go-version/iyear/tdl?style=flat-square" alt="">
<img src="https://img.shields.io/github/license/iyear/tdl?style=flat-square" alt="">
<img src="https://img.shields.io/github/actions/workflow/status/iyear/tdl/master.yml?branch=master&style=flat-square" alt="">
<img src="https://img.shields.io/github/v/release/iyear/tdl?color=red&style=flat-square" alt="">
<img src="https://img.shields.io/github/downloads/iyear/tdl/total?style=flat-square" alt="">
</p>
#### 特性:
- 单文件启动
- 低资源占用
- 吃满你的带宽
- 比官方客户端更快
- 支持从受保护的会话中下载文件
- 具有自动回退和消息路由的转发功能
- 支持上传文件至 Telegram
- 导出历史消息/成员/订阅者数据至 JSON 文件
## 预览
预览中的速度已经达到了代理的限制,同时**速度取决于你是否是付费用户**

## 文档
请参考 [文档](https://docs.iyear.me/tdl/zh/).
## 赞助者

## 贡献者
<a href="https://github.com/iyear/tdl/graphs/contributors">
<img src="https://contrib.rocks/image?repo=iyear/tdl&max=750&columns=20" alt="contributors"/>
</a>
## 协议
AGPL-3.0 License
================================================
FILE: app/chat/export.go
================================================
package chat
import (
"context"
"encoding/json"
"fmt"
"os"
"time"
"github.com/expr-lang/expr"
"github.com/fatih/color"
"github.com/go-faster/jx"
"github.com/gotd/td/telegram"
"github.com/gotd/td/telegram/peers"
"github.com/gotd/td/telegram/query"
"github.com/gotd/td/telegram/query/messages"
"github.com/gotd/td/tg"
"github.com/jedib0t/go-pretty/v6/progress"
"go.uber.org/multierr"
"github.com/iyear/tdl/core/storage"
"github.com/iyear/tdl/core/tmedia"
"github.com/iyear/tdl/core/util/tutil"
"github.com/iyear/tdl/pkg/prog"
"github.com/iyear/tdl/pkg/texpr"
)
//go:generate go-enum --names --values --flag --nocase
type ExportOptions struct {
Type ExportType
Chat string
Thread int // topic id in forum, message id in group
Input []int
Output string
Filter string
OnlyMedia bool
WithContent bool
Raw bool
All bool
}
type Message struct {
ID int `json:"id"`
Type string `json:"type"`
File string `json:"file"`
Date int `json:"date,omitempty"`
Text string `json:"text,omitempty"`
Raw *tg.Message `json:"raw,omitempty"`
}
// ExportType
// ENUM(time, id, last)
type ExportType int
func Export(ctx context.Context, c *telegram.Client, kvd storage.Storage, opts ExportOptions) (rerr error) {
// only output available fields
if opts.Filter == "-" {
fg := texpr.NewFieldsGetter(nil)
fields, err := fg.Walk(&texpr.EnvMessage{})
if err != nil {
return fmt.Errorf("failed to walk fields: %w", err)
}
fmt.Print(fg.Sprint(fields, true))
return nil
}
filter, err := expr.Compile(opts.Filter, expr.AsBool())
if err != nil {
return fmt.Errorf("failed to compile filter: %w", err)
}
var peer peers.Peer
manager := peers.Options{Storage: storage.NewPeers(kvd)}.Build(c.API())
if opts.Chat == "" { // defaults to me(saved messages)
peer, err = manager.Self(ctx)
} else {
peer, err = tutil.GetInputPeer(ctx, manager, opts.Chat)
}
if err != nil {
return fmt.Errorf("failed to get peer: %w", err)
}
color.Yellow("WARN: Export only generates minimal JSON for tdl download, not for backup.")
color.Cyan("Occasional suspensions are due to Telegram rate limitations, please wait a moment.")
fmt.Println()
color.Blue("Type: %s | Input: %v", opts.Type, opts.Input)
pw := prog.New(progress.FormatNumber)
pw.SetUpdateFrequency(200 * time.Millisecond)
pw.Style().Visibility.TrackerOverall = false
pw.Style().Visibility.ETA = false
pw.Style().Visibility.Percentage = false
tracker := prog.AppendTracker(pw, progress.FormatNumber, fmt.Sprintf("%s-%d", peer.VisibleName(), peer.ID()), 0)
go pw.Render()
var q messages.Query
switch {
case opts.Thread != 0: // topic messages, reply messages
q = query.NewQuery(c.API()).Messages().GetReplies(peer.InputPeer()).MsgID(opts.Thread)
default: // history
q = query.NewQuery(c.API()).Messages().GetHistory(peer.InputPeer())
}
iter := messages.NewIterator(q, 100)
switch opts.Type {
case ExportTypeTime:
iter = iter.OffsetDate(opts.Input[1] + 1)
case ExportTypeId:
iter = iter.OffsetID(opts.Input[1] + 1) // #89: retain the last msg id
case ExportTypeLast:
}
f, err := os.Create(opts.Output)
if err != nil {
return err
}
defer multierr.AppendInvoke(&rerr, multierr.Close(f))
enc := jx.NewStreamingEncoder(f, 512)
defer multierr.AppendInvoke(&rerr, multierr.Close(enc))
// process thread is reply type and peer is broadcast channel,
// so we need to set discussion group id instead of broadcast id
id := peer.ID()
if p, ok := peer.(peers.Channel); opts.Thread != 0 && ok && p.IsBroadcast() {
bc, _ := p.ToBroadcast()
raw, err := bc.FullRaw(ctx)
if err != nil {
return fmt.Errorf("failed to get broadcast full raw: %w", err)
}
if id, ok = raw.GetLinkedChatID(); !ok {
return fmt.Errorf("no linked group")
}
}
enc.ObjStart()
defer enc.ObjEnd()
enc.Field("id", func(e *jx.Encoder) { e.Int64(id) })
enc.FieldStart("messages")
enc.ArrStart()
defer enc.ArrEnd()
count := int64(0)
loop:
for iter.Next(ctx) {
msg := iter.Value()
switch opts.Type {
case ExportTypeTime:
if msg.Msg.GetDate() < opts.Input[0] {
break loop
}
case ExportTypeId:
if msg.Msg.GetID() < opts.Input[0] {
break loop
}
case ExportTypeLast:
if count >= int64(opts.Input[0]) {
break loop
}
}
m, ok := msg.Msg.(*tg.Message)
if !ok {
continue
}
// only get media messages
media, ok := tmedia.GetMedia(m)
if !ok && !opts.All {
continue
}
b, err := texpr.Run(filter, texpr.ConvertEnvMessage(m))
if err != nil {
return fmt.Errorf("failed to run filter: %w", err)
}
if !b.(bool) { // filtered
continue
}
fileName := ""
if media != nil { // #207
fileName = media.Name
}
t := &Message{
ID: m.ID,
Type: "message",
File: fileName,
}
if opts.WithContent {
t.Date = m.Date
t.Text = m.Message
}
if opts.Raw {
t.Raw = m
}
mb, err := json.Marshal(t)
if err != nil {
return fmt.Errorf("failed to marshal message: %w", err)
}
enc.Raw(mb)
count++
tracker.SetValue(count)
}
if err = iter.Err(); err != nil {
return err
}
tracker.MarkAsDone()
prog.Wait(ctx, pw)
return nil
}
================================================
FILE: app/chat/export_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 chat
import (
"fmt"
"strings"
)
const (
// ExportTypeTime is a ExportType of type Time.
ExportTypeTime ExportType = iota
// ExportTypeId is a ExportType of type Id.
ExportTypeId
// ExportTypeLast is a ExportType of type Last.
ExportTypeLast
)
var ErrInvalidExportType = fmt.Errorf("not a valid ExportType, try [%s]", strings.Join(_ExportTypeNames, ", "))
const _ExportTypeName = "timeidlast"
var _ExportTypeNames = []string{
_ExportTypeName[0:4],
_ExportTypeName[4:6],
_ExportTypeName[6:10],
}
// ExportTypeNames returns a list of possible string values of ExportType.
func ExportTypeNames() []string {
tmp := make([]string, len(_ExportTypeNames))
copy(tmp, _ExportTypeNames)
return tmp
}
// ExportTypeValues returns a list of the values for ExportType
func ExportTypeValues() []ExportType {
return []ExportType{
ExportTypeTime,
ExportTypeId,
ExportTypeLast,
}
}
var _ExportTypeMap = map[ExportType]string{
ExportTypeTime: _ExportTypeName[0:4],
ExportTypeId: _ExportTypeName[4:6],
ExportTypeLast: _ExportTypeName[6:10],
}
// String implements the Stringer interface.
func (x ExportType) String() string {
if str, ok := _ExportTypeMap[x]; ok {
return str
}
return fmt.Sprintf("ExportType(%d)", x)
}
// IsValid provides a quick way to determine if the typed value is
// part of the allowed enumerated values
func (x ExportType) IsValid() bool {
_, ok := _ExportTypeMap[x]
return ok
}
var _ExportTypeValue = map[string]ExportType{
_ExportTypeName[0:4]: ExportTypeTime,
strings.ToLower(_ExportTypeName[0:4]): ExportTypeTime,
_ExportTypeName[4:6]: ExportTypeId,
strings.ToLower(_ExportTypeName[4:6]): ExportTypeId,
_ExportTypeName[6:10]: ExportTypeLast,
strings.ToLower(_ExportTypeName[6:10]): ExportTypeLast,
}
// ParseExportType attempts to convert a string to a ExportType.
func ParseExportType(name string) (ExportType, error) {
if x, ok := _ExportTypeValue[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 := _ExportTypeValue[strings.ToLower(name)]; ok {
return x, nil
}
return ExportType(0), fmt.Errorf("%s is %w", name, ErrInvalidExportType)
}
// Set implements the Golang flag.Value interface func.
func (x *ExportType) Set(val string) error {
v, err := ParseExportType(val)
*x = v
return err
}
// Get implements the Golang flag.Getter interface func.
func (x *ExportType) Get() interface{} {
return *x
}
// Type implements the github.com/spf13/pFlag Value interface.
func (x *ExportType) Type() string {
return "ExportType"
}
================================================
FILE: app/chat/ls.go
================================================
package chat
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/expr-lang/expr"
"github.com/fatih/color"
"github.com/go-faster/errors"
"github.com/gotd/td/telegram"
"github.com/gotd/td/telegram/message/peer"
"github.com/gotd/td/telegram/peers"
"github.com/gotd/td/telegram/query/dialogs"
"github.com/gotd/td/tg"
"github.com/mattn/go-runewidth"
"go.uber.org/zap"
"github.com/iyear/tdl/core/logctx"
"github.com/iyear/tdl/core/storage"
"github.com/iyear/tdl/core/util/tutil"
"github.com/iyear/tdl/pkg/texpr"
)
//go:generate go-enum --names --values --flag --nocase
type Dialog struct {
ID int64 `json:"id" comment:"ID of dialog"`
Type string `json:"type" comment:"Type of dialog. Can be 'private', 'channel' or 'group'"`
VisibleName string `json:"visible_name,omitempty" comment:"Title of channel and group, first and last name of user. If empty, output '-'"`
Username string `json:"username,omitempty" comment:"Username of dialog. If empty, output '-'"`
Topics []Topic `json:"topics,omitempty" comment:"Topics of dialog. If not set, output '-'"`
}
type Topic struct {
ID int `json:"id" comment:"ID of topic"`
Title string `json:"title" comment:"Title of topic"`
}
// ListOutput
// ENUM(table, json)
type ListOutput int
// External designation, different from Telegram mtproto
const (
DialogGroup = "group"
DialogPrivate = "private"
DialogChannel = "channel"
DialogUnknown = "unknown"
)
type ListOptions struct {
Output ListOutput
Filter string
}
func List(ctx context.Context, c *telegram.Client, kvd storage.Storage, opts ListOptions) error {
log := logctx.From(ctx)
// align output
runewidth.EastAsianWidth = false
runewidth.DefaultCondition.EastAsianWidth = false
// output available fields
if opts.Filter == "-" {
fg := texpr.NewFieldsGetter(nil)
fields, err := fg.Walk(&Dialog{})
if err != nil {
return fmt.Errorf("failed to walk fields: %w", err)
}
fmt.Print(fg.Sprint(fields, true))
return nil
}
// compile filter
filter, err := expr.Compile(opts.Filter, expr.AsBool())
if err != nil {
return fmt.Errorf("failed to compile filter: %w", err)
}
// Manually iterate through dialogs to handle errors gracefully
// This allows us to skip problematic dialogs (deleted/inaccessible channels)
// rather than failing completely when ExtractPeer fails
dialogs, skipped := fetchDialogsWithErrorHandling(ctx, c.API())
if skipped > 0 {
log.Warn("skipped problematic dialogs during iteration",
zap.Int("skipped", skipped),
zap.Int("fetched", len(dialogs)))
}
blocked, err := tutil.GetBlockedDialogs(ctx, c.API())
if err != nil {
return err
}
manager := peers.Options{Storage: storage.NewPeers(kvd)}.Build(c.API())
result := make([]*Dialog, 0, len(dialogs))
for _, d := range dialogs {
id := tutil.GetInputPeerID(d.Peer)
// we can update our access hash state if there is any new peer.
if err = applyPeers(ctx, manager, d.Entities, id); err != nil {
log.Warn("failed to apply peer updates", zap.Int64("id", id), zap.Error(err))
}
// filter blocked peers
if _, ok := blocked[id]; ok {
continue
}
var r *Dialog
switch t := d.Peer.(type) {
case *tg.InputPeerUser:
r = processUser(t.UserID, d.Entities)
case *tg.InputPeerChannel:
r = processChannel(ctx, c.API(), t.ChannelID, d.Entities)
case *tg.InputPeerChat:
r = processChat(t.ChatID, d.Entities)
}
// skip unsupported types
if r == nil {
continue
}
// filter
b, err := texpr.Run(filter, r)
if err != nil {
return fmt.Errorf("failed to run filter: %w", err)
}
if !b.(bool) {
continue
}
result = append(result, r)
}
switch opts.Output {
case ListOutputTable:
printTable(result)
case ListOutputJson:
bytes, err := json.MarshalIndent(result, "", "\t")
if err != nil {
return fmt.Errorf("marshal json: %w", err)
}
fmt.Println(string(bytes))
default:
return fmt.Errorf("unknown output: %s", opts.Output)
}
return nil
}
func printTable(result []*Dialog) {
fmt.Printf("%s %s %s %s %s\n",
trunc("ID", 10),
trunc("Type", 8),
trunc("VisibleName", 20),
trunc("Username", 20),
"Topics")
for _, r := range result {
fmt.Printf("%s %s %s %s %s\n",
trunc(strconv.FormatInt(r.ID, 10), 10),
trunc(r.Type, 8),
trunc(r.VisibleName, 20),
trunc(r.Username, 20),
topicsString(r.Topics))
}
}
func trunc(s string, len int) string {
s = strings.TrimSpace(s)
if s == "" {
s = "-"
}
return runewidth.FillRight(runewidth.Truncate(s, len, "..."), len)
}
func topicsString(topics []Topic) string {
if len(topics) == 0 {
return "-"
}
s := make([]string, 0, len(topics))
for _, t := range topics {
s = append(s, fmt.Sprintf("%d: %s", t.ID, t.Title))
}
return strings.Join(s, ", ")
}
func processUser(id int64, entities peer.Entities) *Dialog {
u, ok := entities.User(id)
if !ok {
return nil
}
return &Dialog{
ID: u.ID,
VisibleName: visibleName(u.FirstName, u.LastName),
Username: u.Username,
Type: DialogPrivate,
Topics: nil,
}
}
func processChannel(ctx context.Context, api *tg.Client, id int64, entities peer.Entities) *Dialog {
c, ok := entities.Channel(id)
if !ok {
return nil
}
d := &Dialog{
ID: c.ID,
VisibleName: c.Title,
Username: c.Username,
}
// channel type
switch {
case c.Broadcast:
d.Type = DialogChannel
case c.Megagroup, c.Gigagroup:
d.Type = DialogGroup
default:
d.Type = DialogUnknown
}
if c.Forum {
topics, err := fetchTopics(ctx, api, c.AsInput())
if err != nil {
logctx.From(ctx).Error("failed to fetch topics",
zap.Int64("channel_id", c.ID),
zap.String("channel_username", c.Username),
zap.Error(err))
return nil
}
d.Topics = topics
}
return d
}
// fetchTopics https://github.com/telegramdesktop/tdesktop/blob/4047f1733decd5edf96d125589f128758b68d922/Telegram/SourceFiles/data/data_forum.cpp#L135
func fetchTopics(ctx context.Context, api *tg.Client, c tg.InputChannelClass) ([]Topic, error) {
log := logctx.From(ctx)
res := make([]Topic, 0)
limit := 100 // why can't we use 500 like tdesktop?
offsetTopic, offsetID, offsetDate := 0, 0, 0
lastOffsetTopic := -1 // Track the last offsetTopic to detect infinite loops
// Track seen offsetTopics to detect cycles
seenOffsets := make(map[int]bool)
for {
// Detect infinite loop: if offsetTopic hasn't changed or we've seen it before
if offsetTopic == lastOffsetTopic && lastOffsetTopic != -1 {
log.Warn("pagination stuck (same offset), breaking loop",
zap.Int("offset_topic", offsetTopic))
break
}
if seenOffsets[offsetTopic] {
log.Warn("pagination cycle detected, breaking loop",
zap.Int("offset_topic", offsetTopic))
break
}
seenOffsets[offsetTopic] = true
lastOffsetTopic = offsetTopic
req := &tg.ChannelsGetForumTopicsRequest{
Channel: c,
Limit: limit,
OffsetTopic: offsetTopic,
OffsetID: offsetID,
OffsetDate: offsetDate,
}
topics, err := api.ChannelsGetForumTopics(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "get forum topics")
}
// If no topics returned, we're done
if len(topics.Topics) == 0 {
break
}
for _, tp := range topics.Topics {
if t, ok := tp.(*tg.ForumTopic); ok {
res = append(res, Topic{
ID: t.ID,
Title: t.Title,
})
offsetTopic = t.ID
}
}
// Safety break if we've collected all topics
if len(res) >= topics.Count {
break
}
// last page
if len(topics.Topics) < limit {
break
}
// Update offset using last message if available
// Use a local variable for length to be absolutely safe against index out of range
msgCount := len(topics.Messages)
if msgCount > 0 {
if lastMsg, ok := topics.Messages[msgCount-1].AsNotEmpty(); ok {
offsetID, offsetDate = lastMsg.GetID(), lastMsg.GetDate()
} else {
log.Debug("no valid message for offset, relying on offsetTopic only",
zap.Int("offset_topic", offsetTopic))
}
} else {
log.Debug("no messages in topics response, relying on offsetTopic only",
zap.Int("offset_topic", offsetTopic),
zap.Int("topics_count", len(topics.Topics)))
}
}
return res, nil
}
func processChat(id int64, entities peer.Entities) *Dialog {
c, ok := entities.Chat(id)
if !ok {
return nil
}
return &Dialog{
ID: c.ID,
VisibleName: c.Title,
Username: "-",
Type: DialogGroup,
Topics: nil,
}
}
func visibleName(first, last string) string {
if first == "" && last == "" {
return ""
}
if first == "" {
return last
}
if last == "" {
return first
}
return first + " " + last
}
func applyPeers(ctx context.Context, manager *peers.Manager, entities peer.Entities, id int64) error {
users := make([]tg.UserClass, 0, 1)
if user, ok := entities.User(id); ok {
users = append(users, user)
}
chats := make([]tg.ChatClass, 0, 1)
if chat, ok := entities.Chat(id); ok {
chats = append(chats, chat)
}
if channel, ok := entities.Channel(id); ok {
chats = append(chats, channel)
}
return manager.Apply(ctx, users, chats)
}
// fetchDialogsWithErrorHandling manually iterates through dialogs using the raw Telegram API
// to gracefully handle errors from problematic dialogs (deleted/inaccessible channels).
// Instead of failing completely when ExtractPeer fails in gotd's iterator, it logs errors
// and continues, skipping bad dialogs.
func fetchDialogsWithErrorHandling(ctx context.Context, api *tg.Client) ([]dialogs.Elem, int) {
log := logctx.From(ctx)
const batchSize = 100
var (
allElems []dialogs.Elem
skipped int
offsetID int
offsetDate int
offsetPeer tg.InputPeerClass = &tg.InputPeerEmpty{}
seen = make(map[int64]bool) // Track seen dialog IDs to prevent duplicates
)
for {
// Fetch a batch of dialogs using raw API
result, err := api.MessagesGetDialogs(ctx, &tg.MessagesGetDialogsRequest{
OffsetDate: offsetDate,
OffsetID: offsetID,
OffsetPeer: offsetPeer,
Limit: batchSize,
})
if err != nil {
log.Error("failed to fetch dialog batch", zap.Error(err))
break
}
var (
dialogsSlice []tg.DialogClass
messages []tg.MessageClass
users []tg.UserClass
chats []tg.ChatClass
)
switch d := result.(type) {
case *tg.MessagesDialogs:
dialogsSlice = d.Dialogs
messages = d.Messages
users = d.Users
chats = d.Chats
case *tg.MessagesDialogsSlice:
dialogsSlice = d.Dialogs
messages = d.Messages
users = d.Users
chats = d.Chats
case *tg.MessagesDialogsNotModified:
// No more dialogs
return allElems, skipped
default:
log.Error("unexpected dialog type", zap.String("type", fmt.Sprintf("%T", result)))
return allElems, skipped
}
if len(dialogsSlice) == 0 {
break
}
// Build entities map for this batch
// Convert slices to maps as required by peer.NewEntities
userMap := make(map[int64]*tg.User)
for _, u := range users {
if user, ok := u.(*tg.User); ok {
userMap[user.ID] = user
}
}
chatMap := make(map[int64]*tg.Chat)
channelMap := make(map[int64]*tg.Channel)
for _, c := range chats {
switch chat := c.(type) {
case *tg.Chat:
chatMap[chat.ID] = chat
case *tg.Channel:
channelMap[chat.ID] = chat
}
}
entities := peer.NewEntities(userMap, chatMap, channelMap)
// Build message map for quick lookup by ID
messageMap := make(map[int]tg.NotEmptyMessage)
for _, msg := range messages {
switch m := msg.(type) {
case *tg.Message:
messageMap[m.ID] = m
case *tg.MessageService:
messageMap[m.ID] = m
}
}
// Process each dialog in this batch
for _, d := range dialogsSlice {
dialog, ok := d.(*tg.Dialog)
if !ok {
continue
}
// Find the peer ID for logging purposes
var peerID int64
switch p := dialog.Peer.(type) {
case *tg.PeerUser:
peerID = p.UserID
case *tg.PeerChat:
peerID = p.ChatID
case *tg.PeerChannel:
peerID = p.ChannelID
default:
log.Error("unknown peer type", zap.String("type", fmt.Sprintf("%T", p)))
skipped++
continue
}
// Skip if we've already seen this dialog (deduplication)
if seen[peerID] {
continue
}
seen[peerID] = true
// Try to extract the peer - THIS IS WHERE THE ORIGINAL ERROR OCCURS
// In gotd's query/dialogs iterator, it calls ExtractPeer without error handling,
// causing a panic when a channel doesn't exist in entities.
// We catch it here and skip the problematic dialog instead.
// See: https://github.com/iyear/tdl/issues/713
inputPeer, err := entities.ExtractPeer(dialog.Peer)
if err != nil {
// This dialog references a channel/chat that doesn't exist in entities
// (likely deleted, user was banned, or channel is inaccessible).
// Log and skip it instead of failing.
log.Warn("skipping dialog with missing peer",
zap.Int64("peer_id", peerID),
zap.String("peer_type", fmt.Sprintf("%T", dialog.Peer)),
zap.Error(err))
skipped++
continue
}
// Get the last message for this dialog from message map
lastMsg := messageMap[dialog.TopMessage]
// Successfully processed this dialog
allElems = append(allElems, dialogs.Elem{
Peer: inputPeer,
Entities: entities,
Dialog: dialog,
Last: lastMsg,
})
}
// Update offset for next batch using the last dialog in dialogsSlice
// (regardless of whether it was successfully processed or skipped)
if len(dialogsSlice) > 0 {
lastDialog, ok := dialogsSlice[len(dialogsSlice)-1].(*tg.Dialog)
if ok {
// Get the message date from message map
var msgDate int
if lastMsg, found := messageMap[lastDialog.TopMessage]; found {
msgDate = lastMsg.GetDate()
}
offsetDate = msgDate
offsetID = lastDialog.TopMessage
// Set offset peer based on dialog peer type
// Try to get access hash from entities if available
switch peerType := lastDialog.Peer.(type) {
case *tg.PeerUser:
if user, ok := entities.User(peerType.UserID); ok {
offsetPeer = &tg.InputPeerUser{
UserID: peerType.UserID,
AccessHash: user.AccessHash,
}
} else {
// Can't continue pagination without access hash
log.Error("failed to get user for offset, stopping pagination",
zap.Int64("user_id", peerType.UserID))
color.Red("Error: failed to get user for offset, stopping pagination. User ID: %d", peerType.UserID)
return allElems, skipped
}
case *tg.PeerChat:
offsetPeer = &tg.InputPeerChat{ChatID: peerType.ChatID}
case *tg.PeerChannel:
if channel, ok := entities.Channel(peerType.ChannelID); ok {
offsetPeer = &tg.InputPeerChannel{
ChannelID: peerType.ChannelID,
AccessHash: channel.AccessHash,
}
} else {
// Can't continue pagination without access hash
log.Error("failed to get channel for offset, stopping pagination",
zap.Int64("channel_id", peerType.ChannelID))
color.Red("Error: failed to get channel for offset, stopping pagination. Channel ID: %d", peerType.ChannelID)
return allElems, skipped
}
}
}
}
// Check if we've fetched all dialogs
// Continue fetching if we got a full batch (there might be more)
if len(dialogsSlice) < batchSize {
// Got less than requested, we've reached the end
break
}
}
return allElems, skipped
}
================================================
FILE: app/chat/ls_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 chat
import (
"fmt"
"strings"
)
const (
// ListOutputTable is a ListOutput of type Table.
ListOutputTable ListOutput = iota
// ListOutputJson is a ListOutput of type Json.
ListOutputJson
)
var ErrInvalidListOutput = fmt.Errorf("not a valid ListOutput, try [%s]", strings.Join(_ListOutputNames, ", "))
const _ListOutputName = "tablejson"
var _ListOutputNames = []string{
_ListOutputName[0:5],
_ListOutputName[5:9],
}
// ListOutputNames returns a list of possible string values of ListOutput.
func ListOutputNames() []string {
tmp := make([]string, len(_ListOutputNames))
copy(tmp, _ListOutputNames)
return tmp
}
// ListOutputValues returns a list of the values for ListOutput
func ListOutputValues() []ListOutput {
return []ListOutput{
ListOutputTable,
ListOutputJson,
}
}
var _ListOutputMap = map[ListOutput]string{
ListOutputTable: _ListOutputName[0:5],
ListOutputJson: _ListOutputName[5:9],
}
// String implements the Stringer interface.
func (x ListOutput) String() string {
if str, ok := _ListOutputMap[x]; ok {
return str
}
return fmt.Sprintf("ListOutput(%d)", x)
}
// IsValid provides a quick way to determine if the typed value is
// part of the allowed enumerated values
func (x ListOutput) IsValid() bool {
_, ok := _ListOutputMap[x]
return ok
}
var _ListOutputValue = map[string]ListOutput{
_ListOutputName[0:5]: ListOutputTable,
strings.ToLower(_ListOutputName[0:5]): ListOutputTable,
_ListOutputName[5:9]: ListOutputJson,
strings.ToLower(_ListOutputName[5:9]): ListOutputJson,
}
// ParseListOutput attempts to convert a string to a ListOutput.
func ParseListOutput(name string) (ListOutput, error) {
if x, ok := _ListOutputValue[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 := _ListOutputValue[strings.ToLower(name)]; ok {
return x, nil
}
return ListOutput(0), fmt.Errorf("%s is %w", name, ErrInvalidListOutput)
}
// Set implements the Golang flag.Value interface func.
func (x *ListOutput) Set(val string) error {
v, err := ParseListOutput(val)
*x = v
return err
}
// Get implements the Golang flag.Getter interface func.
func (x *ListOutput) Get() interface{} {
return *x
}
// Type implements the github.com/spf13/pFlag Value interface.
func (x *ListOutput) Type() string {
return "ListOutput"
}
================================================
FILE: app/chat/users.go
================================================
package chat
import (
"context"
"encoding/json"
"fmt"
"os"
"time"
"github.com/fatih/color"
"github.com/go-faster/errors"
"github.com/go-faster/jx"
"github.com/gotd/td/telegram"
"github.com/gotd/td/telegram/peers"
"github.com/gotd/td/telegram/query/channels/participants"
"github.com/gotd/td/tg"
"github.com/gotd/td/tgerr"
"github.com/jedib0t/go-pretty/v6/progress"
"go.uber.org/multierr"
"github.com/iyear/tdl/core/storage"
"github.com/iyear/tdl/core/util/tutil"
"github.com/iyear/tdl/pkg/prog"
)
type UsersOptions struct {
Chat string
Output string
Raw bool
}
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Phone string `json:"phone"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
func Users(ctx context.Context, c *telegram.Client, kvd storage.Storage, opts UsersOptions) (rerr error) {
manager := peers.Options{Storage: storage.NewPeers(kvd)}.Build(c.API())
if opts.Chat == "" {
return fmt.Errorf("missing domain id")
}
peer, err := tutil.GetInputPeer(ctx, manager, opts.Chat)
if err != nil {
return fmt.Errorf("failed to get peer: %w", err)
}
ch, ok := peer.(peers.Channel)
if !ok {
return fmt.Errorf("invalid type of chat. channels/groups are supported only")
}
color.Cyan("Occasional suspensions are due to Telegram rate limitations, please wait a moment.")
fmt.Println()
f, err := os.Create(opts.Output)
if err != nil {
return err
}
defer multierr.AppendInvoke(&rerr, multierr.Close(f))
enc := jx.NewStreamingEncoder(f, 512)
defer multierr.AppendInvoke(&rerr, multierr.Close(enc))
enc.ObjStart()
defer enc.ObjEnd()
enc.Field("id", func(e *jx.Encoder) { e.Int64(peer.ID()) })
pw := prog.New(progress.FormatNumber)
pw.SetUpdateFrequency(200 * time.Millisecond)
pw.Style().Visibility.TrackerOverall = false
pw.Style().Visibility.ETA = true
pw.Style().Visibility.Percentage = true
go pw.Render()
builder := func() *participants.GetParticipantsQueryBuilder {
return participants.NewQueryBuilder(c.API()).
GetParticipants(ch.InputChannel()).
BatchSize(100)
}
fields := map[string]*participants.GetParticipantsQueryBuilder{
"users": builder(),
"admins": builder().Admins(),
"kicked": builder().Kicked(""),
"banned": builder().Banned(""),
"bots": builder().Bots(),
}
for field, query := range fields {
iter := query.Iter()
if err = outputUsers(ctx, pw, peer, enc, field, iter, opts.Raw); err != nil {
// skip if we get CHAT_ADMIN_REQUIRED error, just export other fields
if tgerr.Is(err, tg.ErrChatAdminRequired) {
continue
}
return fmt.Errorf("failed to output %s: %w", field, err)
}
}
prog.Wait(ctx, pw)
return nil
}
func outputUsers(ctx context.Context,
pw progress.Writer,
peer peers.Peer,
enc *jx.Encoder,
field string,
iter *participants.Iterator,
raw bool,
) error {
total, err := iter.Total(ctx)
if err != nil {
return errors.Wrap(err, "get total count")
}
tracker := prog.AppendTracker(pw,
progress.FormatNumber,
fmt.Sprintf("%s-%d-%s", peer.VisibleName(), peer.ID(), field),
int64(total))
enc.FieldStart(field)
enc.ArrStart()
defer enc.ArrEnd()
for iter.Next(ctx) {
el := iter.Value()
u, ok := el.User()
if !ok {
continue
}
var output any = u
if !raw {
output = convertTelegramUser(u)
}
buf, err := json.Marshal(output)
if err != nil {
return errors.Wrap(err, "marshal user")
}
enc.Raw(buf)
tracker.Increment(1)
}
if err = iter.Err(); err != nil {
return err
}
tracker.MarkAsDone()
return nil
}
func convertTelegramUser(u *tg.User) User {
var dst User
dst.ID = u.ID
dst.Username = u.Username
dst.Phone = u.Phone
dst.FirstName = u.FirstName
dst.LastName = u.LastName
return dst
}
================================================
FILE: app/dl/dl.go
================================================
package dl
import (
"context"
"encoding/json"
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/fatih/color"
"github.com/go-faster/errors"
"github.com/gotd/td/telegram"
"github.com/gotd/td/telegram/peers"
"github.com/spf13/viper"
"go.uber.org/multierr"
"go.uber.org/zap"
"github.com/iyear/tdl/core/dcpool"
"github.com/iyear/tdl/core/downloader"
"github.com/iyear/tdl/core/logctx"
"github.com/iyear/tdl/core/storage"
"github.com/iyear/tdl/core/tclient"
"github.com/iyear/tdl/pkg/consts"
"github.com/iyear/tdl/pkg/key"
"github.com/iyear/tdl/pkg/prog"
"github.com/iyear/tdl/pkg/tmessage"
"github.com/iyear/tdl/pkg/utils"
)
type Options struct {
Dir string
RewriteExt bool
SkipSame bool
Template string
URLs []string
Files []string
Include []string
Exclude []string
Desc bool
Takeout bool
Group bool // auto detect grouped message
// resume opts
Continue, Restart bool
// serve
Serve bool
Port int
}
type parser struct {
Data []string
Parser tmessage.ParseSource
}
func Run(ctx context.Context, c *telegram.Client, kvd storage.Storage, opts Options) (rerr error) {
pool := dcpool.NewPool(c,
int64(viper.GetInt(consts.FlagPoolSize)),
tclient.NewDefaultMiddlewares(ctx, viper.GetDuration(consts.FlagReconnectTimeout))...)
defer multierr.AppendInvoke(&rerr, multierr.Close(pool))
parsers := []parser{
{Data: opts.URLs, Parser: tmessage.FromURL(ctx, pool, kvd, opts.URLs)},
{Data: opts.Files, Parser: tmessage.FromFile(ctx, pool, kvd, opts.Files, true)},
}
dialogs, err := collectDialogs(parsers)
if err != nil {
return err
}
logctx.From(ctx).Debug("Collect dialogs",
zap.Any("dialogs", dialogs))
if opts.Serve {
return serve(ctx, kvd, pool, dialogs, opts.Port, opts.Takeout)
}
manager := peers.Options{Storage: storage.NewPeers(kvd)}.Build(pool.Default(ctx))
it, err := newIter(pool, manager, dialogs, opts, viper.GetDuration(consts.FlagDelay))
if err != nil {
return err
}
if !opts.Restart {
// resume download and ask user to continue
if err = resume(ctx, kvd, it, !opts.Continue); err != nil {
return err
}
} else {
color.Yellow("Restart download by 'restart' flag")
}
defer func() { // save progress
if rerr != nil { // download is interrupted
multierr.AppendInto(&rerr, saveProgress(ctx, kvd, it))
} else { // if finished, we should clear resume key
multierr.AppendInto(&rerr, kvd.Delete(ctx, key.Resume(it.Fingerprint())))
}
}()
dlProgress := prog.New(utils.Byte.FormatBinaryBytes)
dlProgress.SetNumTrackersExpected(it.Total())
prog.EnablePS(ctx, dlProgress)
options := downloader.Options{
Pool: pool,
Threads: viper.GetInt(consts.FlagThreads),
Iter: it,
Progress: newProgress(dlProgress, it, opts),
}
limit := viper.GetInt(consts.FlagLimit)
logctx.From(ctx).Info("Start download",
zap.String("dir", opts.Dir),
zap.Bool("rewrite_ext", opts.RewriteExt),
zap.Bool("skip_same", opts.SkipSame),
zap.Int("threads", options.Threads),
zap.Int("limit", limit))
color.Green("All files will be downloaded to '%s' dir", opts.Dir)
go dlProgress.Render()
defer func() {
prog.Wait(ctx, dlProgress)
// Notify user if any messages were skipped due to deletion
// This is deferred to ensure it shows after progress rendering completes
if skipped := it.SkippedDeleted(); skipped > 0 {
deletedIDs := it.DeletedIDs()
if len(deletedIDs) <= 5 {
// Show all IDs if 5 or fewer
color.Yellow("⚠️ %d message(s) were skipped because they were deleted: %v", skipped, deletedIDs)
} else {
// Show first 5 and indicate there are more
color.Yellow("⚠️ %d message(s) were skipped because they were deleted: %v... and %d more",
skipped, deletedIDs[:5], len(deletedIDs)-5)
}
}
}()
return downloader.New(options).Download(ctx, limit)
}
func collectDialogs(parsers []parser) ([][]*tmessage.Dialog, error) {
var dialogs [][]*tmessage.Dialog
for _, p := range parsers {
d, err := tmessage.Parse(p.Parser)
if err != nil {
return nil, err
}
dialogs = append(dialogs, d)
}
return dialogs, nil
}
func resume(ctx context.Context, kvd storage.Storage, iter *iter, ask bool) error {
logctx.From(ctx).Debug("Check resume key",
zap.String("fingerprint", iter.Fingerprint()))
b, err := kvd.Get(ctx, key.Resume(iter.Fingerprint()))
if err != nil && !errors.Is(err, storage.ErrNotFound) {
return err
}
if len(b) == 0 { // no progress
return nil
}
finished := make(map[int]struct{})
if err = json.Unmarshal(b, &finished); err != nil {
return err
}
// finished is empty, no need to resume
if len(finished) == 0 {
return nil
}
confirm := false
resumeStr := fmt.Sprintf("Found unfinished download, continue from '%d/%d'", len(finished), iter.Total())
if ask {
if err = survey.AskOne(&survey.Confirm{
Message: color.YellowString(resumeStr + "?"),
}, &confirm); err != nil {
return err
}
} else {
color.Yellow(resumeStr)
confirm = true
}
logctx.From(ctx).Debug("Resume download",
zap.Int("finished", len(finished)))
if !confirm {
// clear resume key
return kvd.Delete(ctx, key.Resume(iter.Fingerprint()))
}
iter.SetFinished(finished)
return nil
}
func saveProgress(ctx context.Context, kvd storage.Storage, it *iter) error {
finished := it.Finished()
logctx.From(ctx).Debug("Save progress",
zap.Int("finished", len(finished)))
b, err := json.Marshal(finished)
if err != nil {
return err
}
return kvd.Set(ctx, key.Resume(it.Fingerprint()), b)
}
================================================
FILE: app/dl/elem.go
================================================
package dl
import (
"io"
"os"
"github.com/gotd/td/telegram/peers"
"github.com/gotd/td/tg"
"github.com/iyear/tdl/core/downloader"
"github.com/iyear/tdl/core/tmedia"
)
type iterElem struct {
id int // tracker id for progress tracking
logicalPos int // logical position for resume/finished tracking
from peers.Peer
fromMsg *tg.Message
file *tmedia.Media
to *os.File
opts Options
}
func (i *iterElem) File() downloader.File { return i }
func (i *iterElem) To() io.WriterAt { return i.to }
func (i *iterElem) AsTakeout() bool { return i.opts.Takeout }
func (i *iterElem) Location() tg.InputFileLocationClass { return i.file.InputFileLoc }
func (i *iterElem) Name() string { return i.file.Name }
func (i *iterElem) Size() int64 { return i.file.Size }
func (i *iterElem) DC() int { return i.file.DC }
================================================
FILE: app/dl/iter.go
================================================
package dl
import (
"bytes"
"context"
"crypto/sha256"
"encoding/binary"
"fmt"
"os"
"path/filepath"
"sort"
"sync"
"text/template"
"time"
"github.com/go-faster/errors"
"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/downloader"
"github.com/iyear/tdl/core/logctx"
"github.com/iyear/tdl/core/tmedia"
"github.com/iyear/tdl/core/util/fsutil"
"github.com/iyear/tdl/core/util/tutil"
"github.com/iyear/tdl/pkg/filterMap"
"github.com/iyear/tdl/pkg/tmessage"
"github.com/iyear/tdl/pkg/tplfunc"
"github.com/iyear/tdl/pkg/utils"
)
const tempExt = ".tmp"
type fileTemplate struct {
DialogID int64
MessageID int
MessageDate int64
FileName string
FileCaption string
FileSize string
DownloadDate int64
}
type iter struct {
pool dcpool.Pool
manager *peers.Manager
dialogs []*tmessage.Dialog
tpl *template.Template
include map[string]struct{}
exclude map[string]struct{}
opts Options
delay time.Duration
mu *sync.Mutex
finished map[int]struct{}
fingerprint string
// This param is kept for potential future use but is currently unused.
// preSum []int
logicalPos int // logical position for finished tracking
dialogIndex int // physical position: current dialog in dialogs array
messageIndex int // physical position: current message in dialog.Messages array
// TODO(Hexa): counter is de facto not be used in the codebase, but I perfer to reserve it. The key point is whether it still needs to be atomic or not.
counter *atomic.Int64
skippedDeleted *atomic.Int64 // count of skipped deleted messages
deletedIDs []string // IDs of deleted messages (format: "dialogID/messageID")
elem chan downloader.Elem
err error
}
func newIter(pool dcpool.Pool, manager *peers.Manager, dialog [][]*tmessage.Dialog,
opts Options, delay time.Duration,
) (*iter, error) {
tpl, err := template.New("dl").
Funcs(tplfunc.FuncMap(tplfunc.All...)).
Parse(opts.Template)
if err != nil {
return nil, errors.Wrap(err, "parse template")
}
dialogs := flatDialogs(dialog)
// if msgs is empty, return error to avoid range out of index
if len(dialogs) == 0 {
return nil, errors.Errorf("you must specify at least one message")
}
// include and exclude
includeMap := filterMap.New(opts.Include, fsutil.AddPrefixDot)
excludeMap := filterMap.New(opts.Exclude, fsutil.AddPrefixDot)
// to keep fingerprint stable
sortDialogs(dialogs, opts.Desc)
return &iter{
pool: pool,
manager: manager,
dialogs: dialogs,
opts: opts,
include: includeMap,
exclude: excludeMap,
tpl: tpl,
delay: delay,
mu: &sync.Mutex{},
finished: make(map[int]struct{}),
fingerprint: fingerprint(dialogs),
// This param is kept for potential future use but is currently unused.
// preSum: preSum(dialogs),
logicalPos: 0,
dialogIndex: 0,
messageIndex: 0,
counter: atomic.NewInt64(-1),
skippedDeleted: atomic.NewInt64(0),
deletedIDs: make([]string, 0),
elem: make(chan downloader.Elem, 10), // grouped message buffer
err: nil,
}, nil
}
func (i *iter) Next(ctx context.Context) bool {
select {
case <-ctx.Done():
i.err = ctx.Err()
return false
default:
}
// if delay is set, sleep for a while for each iteration
if i.delay > 0 && (i.dialogIndex+i.messageIndex) > 0 { // skip first delay
time.Sleep(i.delay)
}
if len(i.elem) > 0 { // there are messages(grouped) in channel that not processed
return true
}
for {
ok, skip := i.process(ctx)
if skip {
continue
}
return ok
}
}
func (i *iter) process(ctx context.Context) (ret bool, skip bool) {
i.mu.Lock()
defer i.mu.Unlock()
// end of iteration or error occurred
if i.dialogIndex >= len(i.dialogs) || i.messageIndex >= len(i.dialogs[i.dialogIndex].Messages) || i.err != nil {
return false, false
}
peer, msg := i.dialogs[i.dialogIndex].Peer, i.dialogs[i.dialogIndex].Messages[i.messageIndex]
// Record current logical position before processing
startLogicalPos := i.logicalPos
// Defer physical position increment
defer func() {
if i.messageIndex++; i.dialogIndex < len(i.dialogs) && i.messageIndex >= len(i.dialogs[i.dialogIndex].Messages) {
i.dialogIndex++
i.messageIndex = 0
}
}()
from, err := i.manager.FromInputPeer(ctx, peer)
if err != nil {
i.err = errors.Wrap(err, "resolve from input peer")
return false, false
}
message, err := tutil.GetSingleMessage(ctx, i.pool.Default(ctx), peer, msg)
if err != nil {
// Check if the error is due to a deleted message
if errors.Is(err, tutil.ErrMessageDeleted) {
logctx.From(ctx).Info("Message may be deleted, skipping",
zap.Int64("dialog_id", tutil.GetInputPeerID(peer)),
zap.Int("message_id", msg),
)
i.skippedDeleted.Inc() // increment skipped deleted counter
i.deletedIDs = append(i.deletedIDs, fmt.Sprintf("%d/%d", tutil.GetInputPeerID(peer), msg)) // track deleted message ID
i.logicalPos++ // increment logical position for skipped message
return false, true
}
i.err = errors.Wrap(err, "resolve message")
return false, false
}
if _, ok := message.GetGroupedID(); ok && i.opts.Group {
return i.processGrouped(ctx, message, from, startLogicalPos)
}
// check if finished
if _, ok := i.finished[startLogicalPos]; ok {
i.logicalPos++ // increment logical position even if skipped
return false, true
}
ret, skip = i.processSingle(ctx, message, from, startLogicalPos)
i.logicalPos++ // increment logical position after processing
return ret, skip
}
func (i *iter) processSingle(ctx context.Context, message *tg.Message, from peers.Peer, logicalPos int) (bool, bool) {
item, ok := tmedia.GetMedia(message)
if !ok {
logctx.From(ctx).Warn("Message has no media",
zap.Int64("dialog_id", from.ID()),
zap.Int("message_id", message.ID),
)
return false, true
}
// process include and exclude
ext := filepath.Ext(item.Name)
if _, ok = i.include[ext]; len(i.include) > 0 && !ok {
return false, true
}
if _, ok = i.exclude[ext]; len(i.exclude) > 0 && ok {
return false, true
}
toName := bytes.Buffer{}
err := i.tpl.Execute(&toName, &fileTemplate{
DialogID: from.ID(),
MessageID: message.ID,
MessageDate: int64(message.Date),
FileName: item.Name,
FileCaption: message.Message,
FileSize: utils.Byte.FormatBinaryBytes(item.Size),
DownloadDate: time.Now().Unix(),
})
if err != nil {
i.err = errors.Wrap(err, "execute template")
return false, false
}
if i.opts.SkipSame {
if stat, err := os.Stat(filepath.Join(i.opts.Dir, toName.String())); err == nil {
if fsutil.GetNameWithoutExt(toName.String()) == fsutil.GetNameWithoutExt(stat.Name()) &&
stat.Size() == item.Size {
return false, true
}
}
}
filename := fmt.Sprintf("%s%s", toName.String(), tempExt)
path := filepath.Join(i.opts.Dir, filename)
// #113. If path contains dirs, create it. So now we support nested dirs.
if err = os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
i.err = errors.Wrap(err, "create dir")
return false, false
}
to, err := os.Create(path)
if err != nil {
i.err = errors.Wrap(err, "create file")
return false, false
}
i.elem <- &iterElem{
id: int(i.counter.Inc()),
logicalPos: logicalPos,
from: from,
fromMsg: message,
file: item,
to: to,
opts: i.opts,
}
return true, false
}
func (i *iter) processGrouped(ctx context.Context, message *tg.Message, from peers.Peer, startLogicalPos int) (bool, bool) {
grouped, err := tutil.GetGroupedMessages(ctx, i.pool.Default(ctx), from.InputPeer(), message)
if err != nil {
i.err = errors.Wrapf(err, "resolve grouped message %d/%d", from.ID(), message.ID)
return false, false
}
hasValid := false
for idx, msg := range grouped {
logicalPos := startLogicalPos + idx
// check if this grouped message is already finished
if _, ok := i.finished[logicalPos]; ok {
continue
}
ret, skip := i.processSingle(ctx, msg, from, logicalPos)
// if processSingle encounters a fatal error (not just skip), propagate it
if !ret && !skip {
// i.err should already be set by processSingle
return false, false
}
if ret {
hasValid = true
}
}
// increment logical position by the number of messages in the group
i.logicalPos += len(grouped)
return hasValid, !hasValid
}
func (i *iter) Value() downloader.Elem {
return <-i.elem
}
func (i *iter) Err() error {
return i.err
}
func (i *iter) SetFinished(finished map[int]struct{}) {
i.mu.Lock()
defer i.mu.Unlock()
i.finished = finished
}
func (i *iter) Finished() map[int]struct{} {
i.mu.Lock()
defer i.mu.Unlock()
return i.finished
}
func (i *iter) Fingerprint() string {
return i.fingerprint
}
func (i *iter) Finish(id int) {
i.mu.Lock()
defer i.mu.Unlock()
i.finished[id] = struct{}{}
}
func (i *iter) Total() int {
i.mu.Lock()
defer i.mu.Unlock()
total := 0
for _, m := range i.dialogs {
total += len(m.Messages)
}
return total
}
func (i *iter) SkippedDeleted() int64 {
return i.skippedDeleted.Load()
}
func (i *iter) DeletedIDs() []string {
i.mu.Lock()
defer i.mu.Unlock()
return i.deletedIDs
}
// positionToLogicalIndex converts physical position (dialogIndex, messageIndex) to logical index
// This method is kept for potential future use but is currently unused.
// func (i *iter) positionToLogicalIndex(dialogIdx, messageIdx int) int {
// return i.preSum[dialogIdx] + messageIdx
// }
func flatDialogs(dialogs [][]*tmessage.Dialog) []*tmessage.Dialog {
res := make([]*tmessage.Dialog, 0)
for _, d := range dialogs {
if len(d) == 0 {
continue
}
res = append(res, d...)
}
return res
}
func sortDialogs(dialogs []*tmessage.Dialog, desc bool) {
sort.Slice(dialogs, func(i, j int) bool {
return tutil.GetInputPeerID(dialogs[i].Peer) <
tutil.GetInputPeerID(dialogs[j].Peer) // increasing order
})
for _, m := range dialogs {
sort.Slice(m.Messages, func(i, j int) bool {
if desc {
return m.Messages[i] > m.Messages[j]
}
return m.Messages[i] < m.Messages[j]
})
}
}
// preSum of dialogs
// This method is kept for potential future use but is currently unused.
// func preSum(dialogs []*tmessage.Dialog) []int {
// sum := make([]int, len(dialogs)+1)
// for i, m := range dialogs {
// sum[i+1] = sum[i] + len(m.Messages)
// }
// return sum
// }
func fingerprint(dialogs []*tmessage.Dialog) string {
endian := binary.BigEndian
buf, b := &bytes.Buffer{}, make([]byte, 8)
for _, m := range dialogs {
endian.PutUint64(b, uint64(tutil.GetInputPeerID(m.Peer)))
buf.Write(b)
for _, msg := range m.Messages {
endian.PutUint64(b, uint64(msg))
buf.Write(b)
}
}
return fmt.Sprintf("%x", sha256.Sum256(buf.Bytes()))
}
================================================
FILE: app/dl/iter_test.go
================================================
package dl
import (
"context"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestIterDeletedMessageHandling verifies that deleted messages are handled correctly
// without causing the program to crash. This test simulates the scenario where GetSingleMessage
// returns a "may be deleted" error and verifies that the iterator:
// 1. Does not return a fatal error
// 2. Skips the deleted message
// 3. Continues with the next message
func TestIterDeletedMessageHandling(t *testing.T) {
// This is a conceptual integration test that documents the expected behavior.
// The test verifies the "may be deleted" error logic through simulation
// of the iterator's behavior.
t.Run("error message contains 'may be deleted'", func(t *testing.T) {
// Simulate the error returned by GetSingleMessage when a message is deleted
err := createDeletedMessageError(123, 456)
// Verify that the error contains the string "may be deleted"
assert.Contains(t, err.Error(), "may be deleted",
"Error should contain 'may be deleted'")
// Verify that this is the type of error handled in iter.go:176
isDeletedError := strings.Contains(err.Error(), "may be deleted")
assert.True(t, isDeletedError,
"Error should be recognized as a deleted message")
})
t.Run("deleted message error should be skipped", func(t *testing.T) {
// Simulate the behavior of iter.go process when encountering a deleted message
err := createDeletedMessageError(123, 456)
// Simulate the logic in iter.go:176-182
shouldSkip := false
shouldReturnFatalError := false
if strings.Contains(err.Error(), "may be deleted") {
// This is the behavior implemented in iter.go
shouldSkip = true
shouldReturnFatalError = false
} else {
shouldReturnFatalError = true
}
assert.True(t, shouldSkip,
"Deleted message should be skipped")
assert.False(t, shouldReturnFatalError,
"Should not return a fatal error for deleted messages")
})
t.Run("process continues after deleted message", func(t *testing.T) {
// Simulate a sequence of messages where one is deleted
messages := []struct {
id int
deleted bool
}{
{id: 1, deleted: false},
{id: 2, deleted: true}, // Deleted message
{id: 3, deleted: false},
}
processedCount := 0
skippedCount := 0
for _, msg := range messages {
if msg.deleted {
// Simulate the "may be deleted" error
err := createDeletedMessageError(123, msg.id)
// Verify it's handled correctly
if strings.Contains(err.Error(), "may be deleted") {
skippedCount++
// Process continues (no return or panic)
continue
}
}
processedCount++
}
assert.Equal(t, 2, processedCount,
"Should process 2 messages (1 and 3)")
assert.Equal(t, 1, skippedCount,
"Should skip 1 message (2)")
})
}
// TestIterLogicalPositionIncrement verifies that the logical position is incremented
// correctly even when a message is skipped
func TestIterLogicalPositionIncrement(t *testing.T) {
t.Run("logical position increments on skip", func(t *testing.T) {
// Simulate the logical position behavior
logicalPos := 0
// Normal message - processed
logicalPos++
assert.Equal(t, 1, logicalPos)
// Deleted message - skipped but position increments (iter.go:181)
err := createDeletedMessageError(123, 456)
if strings.Contains(err.Error(), "may be deleted") {
logicalPos++ // This is the behavior in iter.go:181
}
assert.Equal(t, 2, logicalPos,
"Logical position should increment even for skipped messages")
// Normal message - processed
logicalPos++
assert.Equal(t, 3, logicalPos)
})
}
// TestIterNoFatalErrorOnDeletedMessage verifies that no fatal error is set
// when encountering a deleted message
func TestIterNoFatalErrorOnDeletedMessage(t *testing.T) {
t.Run("no fatal error set on deleted message", func(t *testing.T) {
var fatalError error
// Simulate encountering a deleted message
err := createDeletedMessageError(123, 456)
// Simulate the error handling logic in iter.go:175-186
if strings.Contains(err.Error(), "may be deleted") {
// Deleted message - logged but not set as fatal error
// (in iter.go:177-182 only a warning log is made)
// fatalError remains nil
} else {
// Other errors are set as fatal
fatalError = err
}
assert.Nil(t, fatalError,
"Should not set a fatal error for deleted messages")
})
}
// TestIterReturnValues verifies the correct return values from the process function
func TestIterReturnValues(t *testing.T) {
t.Run("process returns correct values for deleted message", func(t *testing.T) {
// Simulate the return values of process() in iter.go:146
// when encountering a deleted message
err := createDeletedMessageError(123, 456)
var ret, skip bool
// Simulate the logic in iter.go:176-182
if strings.Contains(err.Error(), "may be deleted") {
ret = false // No valid element to process
skip = true // Skip this message and continue
}
assert.False(t, ret,
"ret should be false for deleted messages")
assert.True(t, skip,
"skip should be true for deleted messages")
})
}
// createDeletedMessageError simulates the error returned by tutil.GetSingleMessage
// when a message has been deleted (see tutil.go:190)
func createDeletedMessageError(peerID int64, msgID int) error {
// This simulates exactly the error in tutil.go:190
return &deletedMessageError{
peerID: peerID,
msgID: msgID,
}
}
// deletedMessageError is a custom error type that simulates the error
// returned by errors.Errorf in tutil.go:190
type deletedMessageError struct {
peerID int64
msgID int
}
func (e *deletedMessageError) Error() string {
// This corresponds exactly to the format in tutil.go:190
return "the message " + string(rune(e.peerID)) + "/" + string(rune(e.msgID)) + " may be deleted"
}
// TestDeletedMessageErrorFormat verifies that the error format is correct
func TestDeletedMessageErrorFormat(t *testing.T) {
err := createDeletedMessageError(123, 456)
require.NotNil(t, err)
// Verify that the error contains the key string
assert.Contains(t, err.Error(), "may be deleted",
"Error should contain 'may be deleted'")
}
// BenchmarkDeletedMessageDetection measures the performance of detecting
// deleted messages
func BenchmarkDeletedMessageDetection(b *testing.B) {
err := createDeletedMessageError(123, 456)
errMsg := err.Error()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = strings.Contains(errMsg, "may be deleted")
}
}
// TestIterContextCancellation verifies that the iterator correctly handles
// context cancellation even during deleted message handling
func TestIterContextCancellation(t *testing.T) {
t.Run("context cancellation during deleted message handling", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancel()
// Wait for context to be cancelled
time.Sleep(5 * time.Millisecond)
// Verify that the context is cancelled
select {
case <-ctx.Done():
assert.NotNil(t, ctx.Err(),
"Context should be cancelled")
default:
t.Fatal("Context should be cancelled")
}
})
}
================================================
FILE: app/dl/progress.go
================================================
package dl
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/fatih/color"
"github.com/gabriel-vasile/mimetype"
"github.com/go-faster/errors"
pw "github.com/jedib0t/go-pretty/v6/progress"
"github.com/iyear/tdl/core/downloader"
"github.com/iyear/tdl/core/util/fsutil"
"github.com/iyear/tdl/pkg/prog"
"github.com/iyear/tdl/pkg/utils"
)
type progress struct {
pw pw.Writer
trackers *sync.Map // map[ID]*pw.Tracker
opts Options
it *iter
}
func newProgress(p pw.Writer, it *iter, opts Options) *progress {
return &progress{
pw: p,
trackers: &sync.Map{},
opts: opts,
it: it,
}
}
func (p *progress) OnAdd(elem downloader.Elem) {
tracker := prog.AppendTracker(p.pw, utils.Byte.FormatBinaryBytes, p.processMessage(elem), elem.File().Size())
p.trackers.Store(elem.(*iterElem).id, tracker)
}
func (p *progress) OnDownload(elem downloader.Elem, state downloader.ProgressState) {
tracker, ok := p.trackers.Load(elem.(*iterElem).id)
if !ok {
return
}
t := tracker.(*pw.Tracker)
t.UpdateTotal(state.Total)
t.SetValue(state.Downloaded)
}
func (p *progress) OnDone(elem downloader.Elem, err error) {
e := elem.(*iterElem)
tracker, ok := p.trackers.Load(e.id)
if !ok {
return
}
t := tracker.(*pw.Tracker)
if err := e.to.Close(); err != nil {
p.fail(t, elem, errors.Wrap(err, "close file"))
return
}
if err != nil {
if !errors.Is(err, context.Canceled) { // don't report user cancel
p.fail(t, elem, errors.Wrap(err, "progress"))
}
_ = os.Remove(e.to.Name()) // just try to remove temp file, ignore error
return
}
p.it.Finish(e.logicalPos)
if err := p.donePost(e); err != nil {
p.fail(t, elem, errors.Wrap(err, "post file"))
return
}
}
func (p *progress) donePost(elem *iterElem) error {
newfile := strings.TrimSuffix(filepath.Base(elem.to.Name()), tempExt)
if p.opts.RewriteExt {
mime, err := mimetype.DetectFile(elem.to.Name())
if err != nil {
return errors.Wrap(err, "detect mime")
}
ext := mime.Extension()
if ext != "" && (filepath.Ext(newfile) != ext) {
newfile = fsutil.GetNameWithoutExt(newfile) + ext
}
}
newpath := filepath.Join(filepath.Dir(elem.to.Name()), newfile)
if err := os.Rename(elem.to.Name(), newpath); err != nil {
return errors.Wrap(err, "rename file")
}
// Set file modification time to message date if available
if elem.file.Date > 0 {
fileTime := time.Unix(elem.file.Date, 0)
if err := os.Chtimes(newpath, fileTime, fileTime); err != nil {
return errors.Wrap(err, "set file time")
}
}
return nil
}
func (p *progress) fail(t *pw.Tracker, elem downloader.Elem, err error) {
p.pw.Log(color.RedString("%s error: %s", p.elemString(elem), err.Error()))
t.MarkAsErrored()
}
func (p *progress) processMessage(elem downloader.Elem) string {
return p.elemString(elem)
}
func (p *progress) elemString(elem downloader.Elem) string {
e := elem.(*iterElem)
return fmt.Sprintf("%s(%d):%d -> %s",
e.from.VisibleName(),
e.from.ID(),
e.fromMsg.ID,
strings.TrimSuffix(e.to.Name(), tempExt))
}
================================================
FILE: app/dl/serve.go
================================================
package dl
import (
"bytes"
"context"
_ "embed"
"fmt"
"html/template"
"net/http"
"strconv"
"sync"
"github.com/fatih/color"
"github.com/go-faster/errors"
"github.com/gorilla/mux"
"github.com/gotd/contrib/http_io"
"github.com/gotd/contrib/partio"
"github.com/gotd/contrib/tg_io"
"github.com/gotd/td/telegram/peers"
"github.com/gotd/td/tg"
"github.com/spf13/viper"
"github.com/iyear/tdl/core/dcpool"
"github.com/iyear/tdl/core/logctx"
"github.com/iyear/tdl/core/storage"
"github.com/iyear/tdl/core/tmedia"
"github.com/iyear/tdl/core/util/tutil"
"github.com/iyear/tdl/pkg/consts"
"github.com/iyear/tdl/pkg/tmessage"
)
type media struct {
*tmedia.Media
MIME string
}
//go:embed serve.go.tmpl
var tmpl string
func serve(ctx context.Context,
kvd storage.Storage,
pool dcpool.Pool,
dialogs [][]*tmessage.Dialog,
port int,
takeout bool,
) error {
manager := peers.Options{Storage: storage.NewPeers(kvd)}.Build(pool.Default(ctx))
router := mux.NewRouter()
cache := &sync.Map{} // map[string]*media
router.Handle("/{peer}/{message:[0-9]+}", handler(func(w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
peer := vars["peer"]
messageStr := vars["message"]
var item *media
if t, ok := cache.Load(peer + messageStr); ok {
item = t.(*media)
} else {
message, err := strconv.Atoi(messageStr)
if err != nil {
return errors.Wrap(err, "invalid message id")
}
p, err := tutil.GetInputPeer(ctx, manager, peer)
if err != nil {
return errors.Wrap(err, "resolve peer")
}
msg, err := tutil.GetSingleMessage(ctx, pool.Default(ctx), p.InputPeer(), message)
if err != nil {
return errors.Wrap(err, "resolve message")
}
item, err = convItem(msg)
if err != nil {
return errors.Wrap(err, "convItem")
}
cache.Store(peer+messageStr, item)
}
api := pool.Client(ctx, item.DC)
if takeout {
api = pool.Takeout(ctx, item.DC)
}
u := partio.NewStreamer(
tg_io.NewDownloader(api).ChunkSource(item.Size, item.InputFileLoc),
int64(viper.GetInt(consts.FlagPartSize)))
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, item.Name))
http_io.NewHandler(u, item.Size).
WithContentType(item.MIME).
WithLog(logctx.From(ctx).Named("serve")).
ServeHTTP(w, r)
return nil
}))
items := make([]string, 0)
for _, dialog := range dialogs {
for _, d := range dialog {
for _, m := range d.Messages {
items = append(items, fmt.Sprintf("%d/%d", tutil.GetInputPeerID(d.Peer), m))
}
}
}
list := bytes.NewBuffer(nil)
err := template.Must(template.New("serve.go.tmpl").Parse(tmpl)).Execute(list, items)
if err != nil {
return errors.Wrap(err, "execute template")
}
router.Handle("/", handler(func(w http.ResponseWriter, r *http.Request) error {
_, err := fmt.Fprint(w, list.String())
return err
}))
s := http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: router,
}
go func() {
<-ctx.Done()
_ = s.Shutdown(ctx)
}()
color.Green("(Beta) Serving on http://localhost:%d", port)
return s.ListenAndServe()
}
func handler(h func(w http.ResponseWriter, r *http.Request) error) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := h(w, r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
})
}
func convItem(msg *tg.Message) (*media, error) {
md, ok := tmedia.GetMedia(msg)
if !ok {
return nil, errors.New("message is not a media")
}
mime := ""
switch m := msg.Media.(type) {
case *tg.MessageMediaDocument:
doc, ok := m.Document.AsNotEmpty()
if !ok {
return nil, errors.New("document is empty")
}
mime = doc.MimeType
case *tg.MessageMediaPhoto:
mime = "image/jpeg"
}
return &media{
Media: md,
MIME: mime,
}, nil
}
================================================
FILE: app/dl/serve.go.tmpl
================================================
<!DOCTYPE html>
<html>
<head>
<title>tdl serve(beta)</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.file-list {
text-align: center;
}
ul {
list-style: none;
padding: 0;
}
li {
margin: 10px 0;
}
</style>
</head>
<body>
<div class="file-list">
<h1>Files</h1>
<h2>You can use sniffer to download all files</h2>
<ul>
{{range .}}
<li><a href="{{.}}">{{.}}</a></li>
{{end}}
</ul>
</div>
</body>
</html>
================================================
FILE: app/extension/extension.go
================================================
package extension
import (
"context"
"fmt"
"path/filepath"
"strings"
"github.com/fatih/color"
"github.com/go-faster/errors"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/iyear/tdl/pkg/extensions"
)
var (
colorPrint = func(attrs ...color.Attribute) func(padding int, format string, a ...interface{}) {
return func(padding int, format string, a ...interface{}) {
color.New(attrs...).Print(strings.Repeat(" ", padding) + "• ")
fmt.Printf(format+"\n", a...)
}
}
info = colorPrint(color.FgBlue, color.Bold)
succ = colorPrint(color.FgGreen, color.Bold)
fail = colorPrint(color.FgRed, color.Bold)
)
func List(ctx context.Context, em *extensions.Manager) error {
exts, err := em.List(ctx, false)
if err != nil {
return errors.New("list extensions failed")
}
tb := table.NewWriter()
style := table.StyleColoredDark
tb.SetStyle(style)
tb.AppendHeader(table.Row{"NAME", "AUTHOR", "VERSION"})
for _, e := range exts {
tb.AppendRow(table.Row{normalizeExtName(e.Name()), e.Owner(), e.CurrentVersion()})
}
fmt.Println(tb.Render())
return nil
}
func Install(ctx context.Context, em *extensions.Manager, targets []string, force bool) error {
for _, target := range targets {
info(0, "installing extension %s...", normalizeExtName(target))
if err := em.Install(ctx, target, force); err != nil {
fail(1, "install extension %s failed: %s", normalizeExtName(target), err)
continue
}
if em.DryRun() {
succ(1, "extension %s will be installed", normalizeExtName(target))
} else {
succ(1, "extension %s installed", normalizeExtName(target))
}
}
return nil
}
func Upgrade(ctx context.Context, em *extensions.Manager, targets []string) error {
upgradeAll := len(targets) == 0
exts, err := em.List(ctx, upgradeAll)
if err != nil {
return errors.Wrap(err, "list extensions with metadata")
}
if len(exts) == 0 {
return errors.New("no extensions installed")
}
extMap := make(map[string]extensions.Extension)
for _, e := range exts {
extMap[e.Name()] = e
if upgradeAll {
targets = append(targets, e.Name())
}
}
for _, target := range targets {
e, ok := extMap[strings.TrimPrefix(target, extensions.Prefix)]
if !ok {
fail(0, "extension %s not found", normalizeExtName(target))
continue
}
info(0, "upgrading %s...", normalizeExtName(e.Name()))
if err = em.Upgrade(ctx, e); err != nil {
switch {
case errors.Is(err, extensions.ErrAlreadyUpToDate):
succ(1, "extension %s already up-to-date", normalizeExtName(e.Name()))
case errors.Is(err, extensions.ErrOnlyGitHub):
fail(1, "extension %s can't be automatically upgraded by tdl", normalizeExtName(e.Name()))
default:
fail(1, "upgrade extension %s failed: %s", normalizeExtName(e.Name()), err)
}
continue
}
if em.DryRun() {
succ(1, "extension %s will be upgraded", normalizeExtName(e.Name()))
} else {
succ(1, "extension %s upgraded", normalizeExtName(e.Name()))
}
}
return nil
}
func Remove(ctx context.Context, em *extensions.Manager, targets []string) error {
exts, err := em.List(ctx, false)
if err != nil {
return errors.Wrap(err, "list extensions")
}
extMap := make(map[string]extensions.Extension)
for _, e := range exts {
extMap[e.Name()] = e
}
for _, target := range targets {
e, ok := extMap[strings.TrimPrefix(target, extensions.Prefix)]
if !ok {
fail(0, "extension %s not found", normalizeExtName(target))
continue
}
if err = em.Remove(e); err != nil {
fail(0, "remove extension %s failed: %s", normalizeExtName(e.Name()), err)
continue
}
if em.DryRun() {
succ(0, "extension %s will be removed", normalizeExtName(e.Name()))
} else {
succ(0, "extension %s removed", normalizeExtName(e.Name()))
}
}
return nil
}
func normalizeExtName(n string) string {
if idx := strings.IndexRune(n, '/'); idx >= 0 {
n = n[idx+1:]
}
if !strings.HasPrefix(n, extensions.Prefix) {
n = extensions.Prefix + n
}
n = strings.TrimSuffix(n, filepath.Ext(n))
return color.New(color.Bold, color.FgCyan).Sprint(n)
}
================================================
FILE: app/forward/elem.go
================================================
package forward
import (
"github.com/gotd/td/telegram/peers"
"github.com/gotd/td/tg"
"github.com/iyear/tdl/core/forwarder"
)
type iterElem struct {
from peers.Peer
msg *tg.Message
to peers.Peer
thread int
modeOverride forwarder.Mode
opts iterOptions
}
func (i *iterElem) Mode() forwarder.Mode {
if i.modeOverride.IsValid() {
return i.modeOverride
}
return i.opts.mode
}
func (i *iterElem) From() peers.Peer { return i.from }
func (i *iterElem) Msg() *tg.Message { return i.msg }
func (i *iterElem) To() peers.Peer { return i.to }
func (i *iterElem) Thread() int { return i.thread }
func (i *iterElem) AsSilent() bool { return i.opts.silent }
func (i *iterElem) AsDryRun() bool { return i.opts.dryRun }
func (i *iterElem) AsGrouped() bool { return i.opts.grouped }
================================================
FILE: app/forward/forward.go
================================================
package forward
import (
"context"
"fmt"
"os"
"reflect"
"strings"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
"github.com/go-faster/errors"
"github.com/gotd/td/telegram"
"github.com/gotd/td/telegram/peers"
pw "github.com/jedib0t/go-pretty/v6/progress"
"github.com/spf13/viper"
"go.uber.org/multierr"
"github.com/iyear/tdl/app/internal/tctx"
"github.com/iyear/tdl/core/dcpool"
"github.com/iyear/tdl/core/forwarder"
"github.com/iyear/tdl/core/storage"
"github.com/iyear/tdl/core/tclient"
"github.com/iyear/tdl/core/util/tutil"
"github.com/iyear/tdl/pkg/consts"
"github.com/iyear/tdl/pkg/prog"
"github.com/iyear/tdl/pkg/texpr"
"github.com/iyear/tdl/pkg/tmessage"
)
type Options struct {
From []string
To string
Edit string
Mode forwarder.Mode
Silent bool
DryRun bool
Single bool
Desc bool
}
func Run(ctx context.Context, c *telegram.Client, kvd storage.Storage, opts Options) (rerr error) {
if opts.To == "-" || opts.Edit == "-" {
fg := texpr.NewFieldsGetter(nil)
fields, err := fg.Walk(exprEnv(nil, nil))
if err != nil {
return fmt.Errorf("failed to walk fields: %w", err)
}
fmt.Print(fg.Sprint(fields, true))
return nil
}
ctx = tctx.WithKV(ctx, kvd)
pool := dcpool.NewPool(c,
int64(viper.GetInt(consts.FlagPoolSize)),
tclient.NewDefaultMiddlewares(ctx, viper.GetDuration(consts.FlagReconnectTimeout))...)
defer multierr.AppendInvoke(&rerr, multierr.Close(pool))
ctx = tctx.WithPool(ctx, pool)
dialogs, err := collectDialogs(ctx, opts.From, opts.Desc)
if err != nil {
return errors.Wrap(err, "collect dialogs")
}
manager := peers.Options{Storage: storage.NewPeers(kvd)}.Build(pool.Default(ctx))
to, err := resolveDest(ctx, manager, opts.To)
if err != nil {
return errors.Wrap(err, "resolve dest peer")
}
edit, err := resolveEdit(opts.Edit)
if err != nil {
return errors.Wrap(err, "resolve edit")
}
fwProgress := prog.New(pw.FormatNumber)
fwProgress.SetNumTrackersExpected(totalMessages(dialogs))
prog.EnablePS(ctx, fwProgress)
fw := forwarder.New(forwarder.Options{
Pool: pool,
Iter: newIter(iterOptions{
manager: manager,
pool: pool,
to: to,
edit: edit,
dialogs: dialogs,
mode: opts.Mode,
silent: opts.Silent,
dryRun: opts.DryRun,
grouped: !opts.Single,
delay: viper.GetDuration(consts.FlagDelay),
}),
Progress: newProgress(fwProgress),
Threads: viper.GetInt(consts.FlagThreads),
})
go fwProgress.Render()
defer prog.Wait(ctx, fwProgress)
return fw.Forward(ctx)
}
func collectDialogs(ctx context.Context, input []string, desc bool) ([]*tmessage.Dialog, error) {
var dialogs []*tmessage.Dialog
for _, p := range input {
var (
d []*tmessage.Dialog
err error
)
switch {
case strings.HasPrefix(p, "http"):
d, err = tmessage.Parse(tmessage.FromURL(ctx, tctx.Pool(ctx), tctx.KV(ctx), []string{p}))
if err != nil {
return nil, errors.Wrap(err, "parse from url")
}
default:
d, err = tmessage.Parse(tmessage.FromFile(ctx, tctx.Pool(ctx), tctx.KV(ctx), []string{p}, false))
if err != nil {
return nil, errors.Wrap(err, "parse from file")
}
}
if desc {
for _, dd := range d {
for i, j := 0, len(dd.Messages)-1; i < j; i, j = i+1, j-1 {
dd.Messages[i], dd.Messages[j] = dd.Messages[j], dd.Messages[i]
}
}
}
dialogs = append(dialogs, d...)
}
return dialogs, nil
}
// resolveDest parses the input string and returns a vm.Program. It can be a CHAT, a text or a file based on expression engine.
func resolveDest(ctx context.Context, manager *peers.Manager, input string) (*vm.Program, error) {
compile := func(i string) (*vm.Program, error) {
// we pass empty peer and message to enable type checking
return expr.Compile(i, expr.Env(exprEnv(nil, nil)))
}
// default
if input == "" {
return compile(`""`)
}
// file
if exp, err := os.ReadFile(input); err == nil {
return compile(string(exp))
}
// chat
if _, err := tutil.GetInputPeer(ctx, manager, input); err == nil {
// convert to const string
return compile(fmt.Sprintf(`"%s"`, input))
}
// text
return compile(input)
}
// resolveEdit returns nil if input is empty, otherwise it returns a vm.Program. It can be a text or a file based on expression engine.
func resolveEdit(input string) (*vm.Program, error) {
compile := func(i string) (*vm.Program, error) {
// we pass empty peer and message to enable type checking
return expr.Compile(i, expr.Env(exprEnv(nil, nil)), expr.AsKind(reflect.String))
}
// no edit, nil program
if input == "" {
return nil, nil
}
// file
if exp, err := os.ReadFile(input); err == nil {
return compile(string(exp))
}
// text
return compile(input)
}
func totalMessages(dialogs []*tmessage.Dialog) int {
var total int
for _, d := range dialogs {
total += len(d.Messages)
}
return total
}
================================================
FILE: app/forward/iter.go
================================================
package forward
import (
"context"
"strings"
"time"
"github.com/expr-lang/expr/vm"
"github.com/go-faster/errors"
"github.com/gotd/td/telegram/message/entity"
"github.com/gotd/td/telegram/message/html"
"github.com/gotd/td/telegram/peers"
"github.com/gotd/td/tg"
"github.com/mitchellh/mapstructure"
"github.com/iyear/tdl/core/dcpool"
"github.com/iyear/tdl/core/forwarder"
"github.com/iyear/tdl/core/util/tutil"
"github.com/iyear/tdl/pkg/texpr"
"github.com/iyear/tdl/pkg/tmessage"
)
type iterOptions struct {
manager *peers.Manager
pool dcpool.Pool
to *vm.Program
edit *vm.Program
dialogs []*tmessage.Dialog
mode forwarder.Mode
silent bool
dryRun bool
grouped bool
delay time.Duration
}
type iter struct {
opts iterOptions
i, j int
elem forwarder.Elem
err error
}
type env struct {
From struct {
ID int64 `comment:"ID of dialog"`
Username string `comment:"Username of dialog"`
VisibleName string `comment:"Title of channel and group, first and last name of user"`
}
Message texpr.EnvMessage
}
func exprEnv(from peers.Peer, msg *tg.Message) env {
e := env{}
if from != nil {
e.From.ID = from.ID()
e.From.Username, _ = from.Username()
e.From.VisibleName = from.VisibleName()
}
if msg != nil {
e.Message = texpr.ConvertEnvMessage(msg)
}
return e
}
type dest struct {
Peer string
Thread int
}
func newIter(opts iterOptions) *iter {
return &iter{
opts: opts,
i: 0,
j: 0,
elem: nil,
err: nil,
}
}
func (i *iter) Next(ctx context.Context) bool {
select {
case <-ctx.Done():
i.err = ctx.Err()
return false
default:
}
// end of iteration or error occurred
if i.i >= len(i.opts.dialogs) || i.err != nil {
return false
}
// if delay is set, sleep for a while for each iteration
if i.opts.delay > 0 && (i.i+i.j) > 0 { // skip first delay
time.Sleep(i.opts.delay)
}
p, m := i.opts.dialogs[i.i].Peer, i.opts.dialogs[i.i].Messages[i.j]
if i.j++; i.j >= len(i.opts.dialogs[i.i].Messages) {
i.i++
i.j = 0
}
from, err := i.opts.manager.FromInputPeer(ctx, p)
if err != nil {
i.err = errors.Wrap(err, "get from peer")
return false
}
msg, err := tutil.GetSingleMessage(ctx, i.opts.pool.Default(ctx), from.InputPeer(), m)
if err != nil {
i.err = errors.Wrapf(err, "get message: %d", m)
return false
}
// message routing
result, err := texpr.Run(i.opts.to, exprEnv(from, msg))
if err != nil {
i.err = errors.Wrap(err, "message routing")
return false
}
var (
to peers.Peer
thread int
)
switch r := result.(type) {
case string:
// pure chat, no reply to, which is a compatible with old version
// and a convenient way to send message to self
to, err = i.resolvePeer(ctx, r)
case map[string]interface{}:
// chat with reply to topic or message
var d dest
if err = mapstructure.WeakDecode(r, &d); err != nil {
i.err = errors.Wrapf(err, "decode dest: %v", result)
return false
}
to, err = i.resolvePeer(ctx, d.Peer)
thread = d.Thread
default:
i.err = errors.Errorf("message router must return string or dest: %T", result)
return false
}
var modeOverride forwarder.Mode = -1 // default value is invalid
// edit message
if i.opts.edit != nil {
result, err = texpr.Run(i.opts.edit, exprEnv(from, msg))
if err != nil {
i.err = errors.Wrap(err, "edit message")
return false
}
r, ok := result.(string)
if !ok {
i.err = errors.Errorf("edit must return string: %T", result)
return false
}
eb := entity.Builder{}
if err = html.HTML(strings.NewReader(r), &eb, html.Options{
UserResolver: nil,
DisableTelegramEscape: false,
}); err != nil {
i.err = errors.Wrap(err, "parse edited message")
return false
}
// modify message
msg.Message, msg.Entities = eb.Complete()
// direct mode can't modify message content, so we force it to be clone mode
modeOverride = forwarder.ModeClone
}
if err != nil {
i.err = errors.Wrapf(err, "resolve dest: %v", result)
return false
}
i.elem = &iterElem{
from: from,
msg: msg,
to: to,
thread: thread,
modeOverride: modeOverride,
opts: i.opts,
}
return true
}
func (i *iter) resolvePeer(ctx context.Context, peer string) (peers.Peer, error) {
if peer == "" { // self
return i.opts.manager.Self(ctx)
}
return tutil.GetInputPeer(ctx, i.opts.manager, peer)
}
func (i *iter) Value() forwarder.Elem {
return i.elem
}
func (i *iter) Err() error {
return i.err
}
================================================
FILE: app/forward/progress.go
================================================
package forward
import (
"fmt"
"strings"
"github.com/fatih/color"
pw "github.com/jedib0t/go-pretty/v6/progress"
"github.com/mattn/go-runewidth"
"github.com/iyear/tdl/core/forwarder"
"github.com/iyear/tdl/pkg/prog"
"github.com/iyear/tdl/pkg/utils"
)
type progress struct {
pw pw.Writer
trackers map[tuple]*pw.Tracker // TODO(iyear): concurrent map
elemName map[int64]string
}
type tuple struct {
from int64
msg int
to int64
}
func newProgress(p pw.Writer) *progress {
return &progress{
pw: p,
trackers: make(map[tuple]*pw.Tracker),
elemName: make(map[int64]string),
}
}
func (p *progress) OnAdd(elem forwarder.Elem) {
tracker := prog.AppendTracker(p.pw, pw.FormatNumber, p.processMessage(elem, false), 1)
p.trackers[p.tuple(elem)] = tracker
}
func (p *progress) OnClone(elem forwarder.Elem, state forwarder.ProgressState) {
tracker, ok := p.trackers[p.tuple(elem)]
if !ok {
return
}
// display re-upload transfer info
tracker.Units.Formatter = utils.Byte.FormatBinaryBytes
tracker.UpdateMessage(p.processMessage(elem, true))
tracker.UpdateTotal(state.Total)
tracker.SetValue(state.Done)
}
func (p *progress) OnDone(elem forwarder.Elem, err error) {
tracker, ok := p.trackers[p.tuple(elem)]
if !ok {
return
}
if err != nil {
p.pw.Log(color.RedString("%s error: %s", p.metaString(elem), err.Error()))
tracker.MarkAsErrored()
return
}
if tracker.Total == 1 {
tracker.Increment(1)
}
tracker.MarkAsDone()
}
func (p *progress) tuple(elem forwarder.Elem) tuple {
return tuple{
from: elem.From().ID(),
msg: elem.Msg().ID,
to: elem.To().ID(),
}
}
func (p *progress) processMessage(elem forwarder.Elem, clone bool) string {
b := &strings.Builder{}
b.WriteString(p.metaString(elem))
if clone {
b.WriteString(" [clone]")
}
return b.String()
}
func (p *progress) metaString(elem forwarder.Elem) string {
// TODO(iyear): better responsive name
if _, ok := p.elemName[elem.From().ID()]; !ok {
p.elemName[elem.From().ID()] = runewidth.Truncate(elem.From().VisibleName(), 15, "...")
}
if _, ok := p.elemName[elem.To().ID()]; !ok {
p.elemName[elem.To().ID()] = runewidth.Truncate(elem.To().VisibleName(), 15, "...")
}
return fmt.Sprintf("%s(%d):%d -> %s(%d)",
p.elemName[elem.From().ID()],
elem.From().ID(),
elem.Msg().ID,
p.elemName[elem.To().ID()],
elem.To().ID())
}
================================================
FILE: app/internal/tctx/tctx.go
================================================
package tctx
import (
"context"
"github.com/iyear/tdl/core/dcpool"
"github.com/iyear/tdl/core/storage"
)
type kvKey struct{}
func KV(ctx context.Context) storage.Storage {
return ctx.Value(kvKey{}).(storage.Storage)
}
func WithKV(ctx context.Context, kv storage.Storage) context.Context {
return context.WithValue(ctx, kvKey{}, kv)
}
type poolKey struct{}
func Pool(ctx context.Context) dcpool.Pool {
return ctx.Value(poolKey{}).(dcpool.Pool)
}
func WithPool(ctx context.Context, pool dcpool.Pool) context.Context {
return context.WithValue(ctx, poolKey{}, pool)
}
================================================
FILE: app/login/code.go
================================================
package login
import (
"context"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/fatih/color"
"github.com/go-faster/errors"
"github.com/gotd/td/telegram/auth"
"github.com/gotd/td/tg"
"github.com/spf13/viper"
"github.com/iyear/tdl/pkg/consts"
"github.com/iyear/tdl/pkg/key"
"github.com/iyear/tdl/pkg/kv"
"github.com/iyear/tdl/pkg/tclient"
)
func Code(ctx context.Context) error {
kvd, err := kv.From(ctx).Open(viper.GetString(consts.FlagNamespace))
if err != nil {
return errors.Wrap(err, "open kv")
}
if err = kvd.Set(ctx, key.App(), []byte(tclient.AppDesktop)); err != nil {
return errors.Wrap(err, "set app")
}
c, err := tclient.New(ctx, tclient.Options{
KV: kvd,
Proxy: viper.GetString(consts.FlagProxy),
NTP: viper.GetString(consts.FlagNTP),
ReconnectTimeout: viper.GetDuration(consts.FlagReconnectTimeout),
UpdateHandler: nil,
}, true)
if err != nil {
return err
}
return c.Run(ctx, func(ctx context.Context) error {
if err = c.Ping(ctx); err != nil {
return err
}
flow := auth.NewFlow(termAuth{}, auth.SendCodeOptions{})
if err = c.Auth().IfNecessary(ctx, flow); err != nil {
return err
}
user, err := c.Self(ctx)
if err != nil {
return err
}
color.Green("Login successfully! ID: %d, Username: %s", user.ID, user.Username)
return nil
})
}
// noSignUp can be embedded to prevent signing up.
type noSignUp struct{}
func (c noSignUp) SignUp(_ context.Context) (auth.UserInfo, error) {
return auth.UserInfo{}, errors.New("don't support sign up Telegram account")
}
func (c noSignUp) AcceptTermsOfService(_ context.Context, tos tg.HelpTermsOfService) error {
return &auth.SignUpRequired{TermsOfService: tos}
}
// termAuth implements authentication via terminal.
type termAuth struct {
noSignUp
}
func (a termAuth) Phone(_ context.Context) (string, error) {
phone := ""
prompt := &survey.Input{
Message: "Enter your phone number:",
Default: "+86 12345678900",
}
if err := survey.AskOne(prompt, &phone, survey.WithValidator(survey.Required)); err != nil {
return "", err
}
color.Blue("Sending Code...")
return strings.TrimSpace(phone), nil
}
func (a termAuth) Password(_ context.Context) (string, error) {
pwd := ""
prompt := &survey.Password{
Message: "Enter 2FA Password:",
}
if err := survey.AskOne(prompt, &pwd, survey.WithValidator(survey.Required)); err != nil {
return "", err
}
return strings.TrimSpace(pwd), nil
}
func (a termAuth) Code(_ context.Context, _ *tg.AuthSentCode) (string, error) {
code := ""
prompt := &survey.Input{
Message: "Enter Code:",
}
if err := survey.AskOne(prompt, &code, survey.WithValidator(survey.Required)); err != nil {
return "", err
}
return strings.TrimSpace(code), nil
}
================================================
FILE: app/login/desktop.go
================================================
package login
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"github.com/AlecAivazis/survey/v2"
"github.com/fatih/color"
"github.com/go-faster/errors"
"github.com/gotd/td/session"
tdtdesktop "github.com/gotd/td/session/tdesktop"
"github.com/spf13/viper"
"github.com/iyear/tdl/core/storage"
"github.com/iyear/tdl/core/util/fsutil"
"github.com/iyear/tdl/pkg/consts"
"github.com/iyear/tdl/pkg/key"
"github.com/iyear/tdl/pkg/kv"
"github.com/iyear/tdl/pkg/tclient"
"github.com/iyear/tdl/pkg/tdesktop"
"github.com/iyear/tdl/pkg/tpath"
)
const tdata = "tdata"
func Desktop(ctx context.Context, opts Options) error {
ns := viper.GetString(consts.FlagNamespace)
kvd, err := kv.From(ctx).Open(ns)
if err != nil {
return errors.Wrap(err, "open kv")
}
desktop, err := findDesktop(opts.Desktop)
if err != nil {
return err
}
color.Blue("Importing session from desktop client: %s", desktop)
accounts, err := tdtdesktop.Read(appendTData(desktop), []byte(opts.Passcode))
if err != nil {
return err
}
infos := make([]string, 0, len(accounts))
infoMap := make(map[string]tdtdesktop.Account)
for _, acc := range accounts {
id := strconv.FormatUint(acc.Authorization.UserID, 10)
infos = append(infos, id)
infoMap[id] = acc
}
fmt.Println()
sel, acc := &survey.Select{
Message: "Choose a user id:",
Options: infos,
Help: "You can get user id from @userinfobot",
}, ""
if err = survey.AskOne(sel, &acc); err != nil {
return err
}
data, err := session.TDesktopSession(infoMap[acc])
if err != nil {
return err
}
loader := &session.Loader{Storage: storage.NewSession(kvd, true)}
if err = loader.Save(ctx, data); err != nil {
return err
}
if err = kvd.Set(ctx, key.App(), []byte(tclient.AppDesktop)); err != nil {
return err
}
color.Green("Import %s successfully to '%s' namespace!", acc, ns)
// logout
confirm, logout := &survey.Confirm{
Message: "Do you want to logout existing desktop session?",
Default: false,
Help: "Logout existing desktop session to separate from imported session, which can prevent session conflict." +
"\n NB: Ensure that you can re-login to desktop client",
}, false
if err = survey.AskOne(confirm, &logout); err != nil {
return err
}
if logout {
if err = forceLogout(infoMap[acc].IDx, desktop); err != nil {
return err
}
color.Green("Logout desktop session of %d successfully! Please re-launch Telegram Desktop client",
infoMap[acc].Authorization.UserID)
}
return nil
}
func findDesktop(desktop string) (string, error) {
if desktop == "" { // auto detect
if desktop = detectAppData(); desktop == "" {
return "", fmt.Errorf("no data found in possible paths, please specify path to Telegram Desktop directory with `-d` flag")
}
return desktop, nil
}
// specified path
stat, err := os.Stat(desktop)
if err != nil {
return "", err
}
if !stat.IsDir() { // process path that points to Telegram executable file
desktop = filepath.Dir(desktop)
}
return desktop, nil
}
func detectAppData() string {
for _, p := range tpath.Desktop.AppData(consts.HomeDir) {
if path := appendTData(p); fsutil.PathExists(path) {
return path
}
}
return ""
}
func appendTData(path string) string {
if filepath.Base(path) != tdata {
path = filepath.Join(path, tdata)
}
return path
}
// forceLogout currently only remove session file
func forceLogout(idx uint32, desktop string) error {
dir := "data"
if idx > 0 {
dir = fmt.Sprintf("data#%d", idx+1)
}
return os.RemoveAll(filepath.Join(appendTData(desktop), tdesktop.FileKey(dir)))
}
================================================
FILE: app/login/login.go
================================================
package login
import (
"context"
"github.com/go-faster/errors"
)
//go:generate go-enum --values --names --flag --nocase
// Type
// ENUM(desktop, code, qr)
type Type int
type Options struct {
Type Type
Desktop string
Passcode string
}
func Run(ctx context.Context, opts Options) error {
switch opts.Type {
case TypeDesktop:
return Desktop(ctx, opts)
case TypeCode:
return Code(ctx)
case TypeQr:
return QR(ctx)
default:
return errors.Errorf("unsupported login type: %s", opts.Type)
}
}
================================================
FILE: app/login/login_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 login
import (
"fmt"
"strings"
)
const (
// TypeDesktop is a Type of type Desktop.
TypeDesktop Type = iota
// TypeCode is a Type of type Code.
TypeCode
// TypeQr is a Type of type Qr.
TypeQr
)
var ErrInvalidType = fmt.Errorf("not a valid Type, try [%s]", strings.Join(_TypeNames, ", "))
const _TypeName = "desktopcodeqr"
var _TypeNames = []string{
_TypeName[0:7],
_TypeName[7:11],
_TypeName[11:13],
}
// TypeNames returns a list of possible string values of Type.
func TypeNames() []string {
tmp := make([]string, len(_TypeNames))
copy(tmp, _TypeNames)
return tmp
}
// TypeValues returns a list of the values for Type
func TypeValues() []Type {
return []Type{
TypeDesktop,
TypeCode,
TypeQr,
}
}
var _TypeMap = map[Type]string{
TypeDesktop: _TypeName[0:7],
TypeCode: _TypeName[7:11],
TypeQr: _TypeName[11:13],
}
// String implements the Stringer interface.
func (x Type) String() string {
if str, ok := _TypeMap[x]; ok {
return str
}
return fmt.Sprintf("Type(%d)", x)
}
// IsValid provides a quick way to determine if the typed value is
// part of the allowed enumerated values
func (x Type) IsValid() bool {
_, ok := _TypeMap[x]
return ok
}
var _TypeValue = map[string]Type{
_TypeName[0:7]: TypeDesktop,
strings.ToLower(_TypeName[0:7]): TypeDesktop,
_TypeName[7:11]: TypeCode,
strings.ToLower(_TypeName[7:11]): TypeCode,
_TypeName[11:13]: TypeQr,
strings.ToLower(_TypeName[11:13]): TypeQr,
}
// ParseType attempts to convert a string to a Type.
func ParseType(name string) (Type, error) {
if x, ok := _TypeValue[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 := _TypeValue[strings.ToLower(name)]; ok {
return x, nil
}
return Type(0), fmt.Errorf("%s is %w", name, ErrInvalidType)
}
// Set implements the Golang flag.Value interface func.
func (x *Type) Set(val string) error {
v, err := ParseType(val)
*x = v
return err
}
// Get implements the Golang flag.Getter interface func.
func (x *Type) Get() interface{} {
return *x
}
// Type implements the github.com/spf13/pFlag Value interface.
func (x *Type) Type() string {
return "Type"
}
================================================
FILE: app/login/qr.go
================================================
package login
import (
"context"
"fmt"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/fatih/color"
"github.com/go-faster/errors"
"github.com/gotd/td/telegram/auth/qrlogin"
"github.com/gotd/td/tg"
"github.com/gotd/td/tgerr"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/skip2/go-qrcode"
"github.com/spf13/viper"
"github.com/iyear/tdl/pkg/consts"
"github.com/iyear/tdl/pkg/key"
"github.com/iyear/tdl/pkg/kv"
"github.com/iyear/tdl/pkg/tclient"
)
func QR(ctx context.Context) error {
kvd, err := kv.From(ctx).Open(viper.GetString(consts.FlagNamespace))
if err != nil {
return errors.Wrap(err, "open kv")
}
if err = kvd.Set(ctx, key.App(), []byte(tclient.AppDesktop)); err != nil {
return errors.Wrap(err, "set app")
}
d := tg.NewUpdateDispatcher()
c, err := tclient.New(ctx, tclient.Options{
KV: kvd,
Proxy: viper.GetString(consts.FlagProxy),
NTP: viper.GetString(consts.FlagNTP),
ReconnectTimeout: viper.GetDuration(consts.FlagReconnectTimeout),
UpdateHandler: d,
}, true)
if err != nil {
return errors.Wrap(err, "create client")
}
return c.Run(ctx, func(ctx context.Context) error {
color.Blue("Scan QR code with your Telegram app...")
var lines int
_, err = c.QR().Auth(ctx, qrlogin.OnLoginToken(d), func(ctx context.Context, token qrlogin.Token) error {
qr, err := qrcode.New(token.URL(), qrcode.Medium)
if err != nil {
return errors.Wrap(err, "create qr")
}
code := qr.ToSmallString(false)
lines = strings.Count(code, "\n")
fmt.Print(code)
fmt.Print(strings.Repeat(text.CursorUp.Sprint(), lines))
return nil
})
// clear qrcode
out := &strings.Builder{}
for i := 0; i < lines; i++ {
out.WriteString(text.EraseLine.Sprint())
out.WriteString(text.CursorDown.Sprint())
}
out.WriteString(text.CursorUp.Sprintn(lines))
fmt.Print(out.String())
if err != nil {
// https://core.telegram.org/api/auth#2fa
if !tgerr.Is(err, "SESSION_PASSWORD_NEEDED") {
return errors.Wrap(err, "qr auth")
}
pwd := ""
prompt := &survey.Password{
Message: "Enter 2FA Password:",
}
if err = survey.AskOne(prompt, &pwd, survey.WithValidator(survey.Required)); err != nil {
return errors.Wrap(err, "2fa password")
}
if _, err = c.Auth().Password(ctx, pwd); err != nil {
return errors.Wrap(err, "2fa auth")
}
}
user, err := c.Self(ctx)
if err != nil {
return errors.Wrap(err, "get self")
}
fmt.Print(text.EraseLine.Sprint())
color.Green("Login successfully! ID: %d, Username: %s", user.ID, user.Username)
return nil
})
}
================================================
FILE: app/migrate/backup.go
================================================
package migrate
import (
"context"
"encoding/json"
"os"
"github.com/fatih/color"
"github.com/go-faster/errors"
"github.com/klauspost/compress/zstd"
"go.uber.org/multierr"
"github.com/iyear/tdl/pkg/kv"
)
func Backup(ctx context.Context, dst string) (rerr error) {
meta, err := kv.From(ctx).MigrateTo()
if err != nil {
return errors.Wrap(err, "read metadata")
}
f, err := os.Create(dst)
if err != nil {
return errors.Wrap(err, "create file")
}
defer multierr.AppendInvoke(&rerr, multierr.Close(f))
enc, err := zstd.NewWriter(f, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
if err != nil {
return errors.Wrap(err, "create zstd encoder")
}
defer multierr.AppendInvoke(&rerr, multierr.Close(enc))
metaB, err := json.Marshal(meta)
if err != nil {
return errors.Wrap(err, "marshal metadata")
}
if _, err = enc.Write(metaB); err != nil {
return errors.Wrap(err, "write metadata")
}
color.Green("Backup successfully, file: %s", dst)
return nil
}
================================================
FILE: app/migrate/migrate.go
================================================
package migrate
import (
"context"
"github.com/AlecAivazis/survey/v2"
"github.com/fatih/color"
"github.com/go-faster/errors"
"github.com/iyear/tdl/pkg/kv"
)
func Migrate(ctx context.Context, to map[string]string) error {
var confirm bool
if err := survey.AskOne(&survey.Confirm{
Message: "It will overwrite the namespace data in the destination storage, continue?",
Default: false,
}, &confirm); err != nil {
return errors.Wrap(err, "confirm")
}
if !confirm {
return nil
}
meta, err := kv.From(ctx).MigrateTo()
if err != nil {
return errors.Wrap(err, "read data")
}
dest, err := kv.NewWithMap(to)
if err != nil {
return errors.Wrap(err, "create dest storage")
}
if err = dest.MigrateFrom(meta); err != nil {
return errors.Wrap(err, "migrate from")
}
color.Green("Migrate successfully.")
for ns := range meta {
color.Green(" - %s", ns)
}
return nil
}
================================================
FILE: app/migrate/recover.go
================================================
package migrate
import (
"bytes"
"context"
"encoding/json"
"os"
"github.com/fatih/color"
"github.com/go-faster/errors"
"github.com/klauspost/compress/zstd"
"go.uber.org/multierr"
"github.com/iyear/tdl/pkg/kv"
)
func Recover(ctx context.Context, file string) (rerr error) {
f, err := os.Open(file)
if err != nil {
return errors.Wrap(err, "open file")
}
defer multierr.AppendInvoke(&rerr, multierr.Close(f))
dec, err := zstd.NewReader(f)
if err != nil {
return errors.Wrap(err, "create zstd decoder")
}
defer dec.Close()
metaB := bytes.NewBuffer(nil)
if _, err = dec.WriteTo(metaB); err != nil {
return err
}
var meta kv.Meta
if err = json.Unmarshal(metaB.Bytes(), &meta); err != nil {
return errors.Wrap(err, "unmarshal metadata")
}
if err = kv.From(ctx).MigrateFrom(meta); err != nil {
return errors.Wrap(err, "migrate from")
}
color.Green("Recover successfully, file: %s", file)
return nil
}
================================================
FILE: app/up/elem.go
================================================
package up
import (
"os"
"path/filepath"
"github.com/gotd/td/telegram/message/entity"
"github.com/gotd/td/telegram/peers"
"github.com/gotd/td/tg"
"github.com/iyear/tdl/core/uploader"
)
type iterElem struct {
file *uploaderFile
thumb *uploaderFile
to peers.Peer
caption *entity.Builder
thread int
asPhoto bool
remove bool
}
func (e *iterElem) File() uploader.File {
return e.file
}
func (e *iterElem) Thumb() (uploader.File, bool) {
if e.thumb == nil {
return nil, false
}
return e.thumb, true
}
func (e *iterElem) Caption() (string, []tg.MessageEntityClass) {
return e.caption.Complete()
}
func (e *iterElem) To() tg.InputPeerClass {
return e.to.InputPeer()
}
func (e *iterElem) Thread() int {
return e.thread
}
func (e *iterElem) AsPhoto() bool {
return e.asPhoto
}
type uploaderFile struct {
*os.File
size int64
}
func (u *uploaderFile) Name() string {
return filepath.Base(u.File.Name())
}
func (u *uploaderFile) Size() int64 {
return u.size
}
================================================
FILE: app/up/iter.go
================================================
package up
import (
"context"
"os"
"strings"
"time"
"github.com/expr-lang/expr/vm"
"github.com/gabriel-vasile/mimetype"
"github.com/go-faster/errors"
"github.com/go-viper/mapstructure/v2"
"github.com/gotd/td/telegram/message/entity"
"github.com/gotd/td/telegram/message/html"
"github.com/gotd/td/telegram/peers"
"github.com/iyear/tdl/core/uploader"
"github.com/iyear/tdl/core/util/mediautil"
"github.com/iyear/tdl/core/util/tutil"
"github.com/iyear/tdl/pkg/texpr"
)
type File struct {
File string
Thumb string
}
type dest struct {
Peer string
Thread int
}
type iter struct {
files []*File
to *vm.Program
caption *vm.Program
chat string
topic int
photo bool
remove bool
delay time.Duration
manager *peers.Manager
cur int
err error
file uploader.Elem
}
func newIter(files []*File, to, caption *vm.Program, chat string, topic int, photo, remove bool, delay time.Duration, manager *peers.Manager) *iter {
return &iter{
files: files,
to: to,
caption: caption,
chat: chat,
topic: topic,
photo: photo,
remove: remove,
delay: delay,
manager: manager,
cur: 0,
err: nil,
file: nil,
}
}
func (i *iter) Next(ctx context.Context) bool {
select {
case <-ctx.Done():
i.err = ctx.Err()
return false
default:
}
if i.cur >= len(i.files) || i.err != nil {
return false
}
// if delay is set, sleep for a while for each iteration
if i.delay > 0 && i.cur > 0 { // skip first delay
time.Sleep(i.delay)
}
cur := i.files[i.cur]
i.cur++
file, err := i.next(ctx, cur)
if err != nil {
i.err = err
return false
}
i.file = file
return true
}
func (i *iter) next(ctx context.Context, cur *File) (*iterElem, error) {
file, err := i.resolveFile(cur.File)
if err != nil {
return nil, errors.Wrap(err, "resolve file")
}
env := exprEnv(ctx, cur)
to, thread, err := i.resolveDest(ctx, env)
if err != nil {
return nil, errors.Wrap(err, "resolve destination")
}
caption, err := i.resolveCaption(env)
if err != nil {
return nil, errors.Wrap(err, "resolve caption")
}
thumb, err := i.resolveThumb(cur.Thumb)
if err != nil {
return nil, errors.Wrap(err, "resolve thumbnail")
}
return &iterElem{
file: file,
thumb: thumb,
to: to,
caption: caption,
thread: thread,
asPhoto: i.photo,
remove: i.remove,
}, nil
}
func (i *iter) resolveFile(path string) (*uploaderFile, error) {
f, err := os.Open(path)
if err != nil {
return nil, errors.Wrap(err, "open file")
}
stat, err := f.Stat()
if err != nil {
return nil, errors.Wrap(err, "stat file")
}
return &uploaderFile{
File: f,
size: stat.Size(),
}, nil
}
func (i *iter) resolveDest(ctx context.Context, env Env) (peers.Peer, int, error) {
if i.chat != "" { // compatible with old version
to, err := i.resolvePeer(ctx, i.chat)
if err != nil {
return nil, 0, errors.Wrap(err, "resolve chat")
}
return to, i.topic, nil
}
// message routing
result, err := texpr.Run(i.to, env)
if err != nil {
return nil, 0, errors.Wrap(err, "parse expression")
}
var (
to peers.Peer
thread int
)
switch r := result.(type) {
case string:
// pure chat, no reply to, which is a compatible with old version
// and a convenient way to send message to self
to, err = i.resolvePeer(ctx, r)
case map[string]interface{}:
// chat with reply to topic or message
var d dest
if err = mapstructure.WeakDecode(r, &d); err != nil {
return nil, 0, errors.Wrapf(err, "decode dest: %v", result)
}
to, err = i.resolvePeer(ctx, d.Peer)
thread = d.Thread
default:
return nil, 0, errors.Errorf("message router must return string or dest: %T", result)
}
if err != nil {
return nil, 0, errors.Wrap(err, "resolve peer")
}
return to, thread, nil
}
func (i *iter) resolvePeer(ctx context.Context, peer string) (peers.Peer, error) {
if peer == "" { // self
return i.manager.Self(ctx)
}
return tutil.GetInputPeer(ctx, i.manager, peer)
}
func (i *iter) resolveCaption(env Env) (*entity.Builder, error) {
// parse caption
captionStr, err := texpr.Run(i.caption, env)
if err != nil {
return nil, errors.Wrap(err, "parse caption")
}
r, ok := captionStr.(string)
if !ok {
return nil, errors.Errorf("caption must return string, got %T", captionStr)
}
caption := &entity.Builder{}
if len(r) > 0 {
if err = html.HTML(strings.NewReader(r), caption, html.Options{
UserResolver: nil,
DisableTelegramEscape: false,
}); err != nil {
return nil, errors.Wrap(err, "parse caption HTML")
}
}
return caption, nil
}
func (i *iter) resolveThumb(path string) (*uploaderFile, error) {
if path == "" {
return nil, nil
}
// has thumbnail
mime, err := mimetype.DetectFile(path)
if err != nil || !mediautil.IsImage(mime.String()) { // TODO(iyear): jpg only
return nil, errors.Wrapf(err, "invalid thumbnail file: %v", path)
}
thumb, err := os.Open(path)
if err != nil {
return nil, errors.Wrap(err, "open thumbnail file")
}
return &uploaderFile{
File: thumb,
size: 0,
}, nil
}
func (i *iter) Value() uploader.Elem {
return i.file
}
func (i *iter) Err() error {
return i.err
}
================================================
FILE: app/up/progress.go
================================================
package up
import (
"fmt"
"os"
"sync"
"github.com/fatih/color"
"github.com/go-faster/errors"
pw "github.com/jedib0t/go-pretty/v6/progress"
"github.com/iyear/tdl/core/uploader"
"github.com/iyear/tdl/pkg/prog"
"github.com/iyear/tdl/pkg/utils"
)
type progress struct {
pw pw.Writer
trackers *sync.Map // map[tuple]*pw.Tracker
}
type tuple struct {
name string
to int64
}
func newProgress(p pw.Writer) *progress {
return &progress{
pw: p,
trackers: &sync.Map{},
}
}
func (p *progress) OnAdd(elem uploader.Elem) {
tracker := prog.AppendTracker(p.pw, utils.Byte.FormatBinaryBytes, p.processMessage(elem), elem.File().Size())
p.trackers.Store(p.tuple(elem), tracker)
}
func (p *progress) OnUpload(elem uploader.Elem, state uploader.ProgressState) {
tracker, ok := p.trackers.Load(p.tuple(elem))
if !ok {
return
}
t := tracker.(*pw.Tracker)
t.UpdateTotal(state.Total)
t.SetValue(state.Uploaded)
}
func (p *progress) OnDone(elem uploader.Elem, err error) {
tracker, ok := p.trackers.Load(p.tuple(elem))
if !ok {
return
}
t := tracker.(*pw.Tracker)
e := elem.(*iterElem)
if err := p.closeFile(e); err != nil {
p.fail(t, elem, errors.Wrap(err, "close file"))
return
}
if err != nil {
p.fail(t, elem, errors.Wrap(err, "progress"))
return
}
if e.remove {
if err := os.Remove(e.file.File.Name()); err != nil {
p.fail(t, elem, errors.Wrap(err, "remove file"))
return
}
}
}
func (p *progress) closeFile(e *iterElem) error {
if err := e.file.Close(); err != nil {
return errors.Wrap(err, "close file")
}
if e.thumb != nil {
if err := e.thumb.Close(); err != nil {
return errors.Wrap(err, "close thumb")
}
}
return nil
}
func (p *progress) fail(t *pw.Tracker, elem uploader.Elem, err error) {
p.pw.Log(color.RedString("%s error: %s", p.elemString(elem), err.Error()))
t.MarkAsErrored()
}
func (p *progress) tuple(elem uploader.Elem) tuple {
return tuple{elem.(*iterElem).file.File.Name(), elem.(*iterElem).to.ID()}
}
func (p *progress) processMessage(elem uploader.Elem) string {
return p.elemString(elem)
}
func (p *progress) elemString(elem uploader.Elem) string {
e := elem.(*iterElem)
return fmt.Sprintf("%s -> %s(%d)", e.file.File.Name(), e.to.VisibleName(), e.to.ID())
}
================================================
FILE: app/up/up.go
================================================
package up
import (
"context"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
"github.com/fatih/color"
"github.com/gabriel-vasile/mimetype"
"github.com/go-faster/errors"
"github.com/gotd/td/telegram"
"github.com/gotd/td/telegram/peers"
"github.com/spf13/viper"
"go.uber.org/multierr"
"go.uber.org/zap"
"github.com/iyear/tdl/core/dcpool"
"github.com/iyear/tdl/core/logctx"
"github.com/iyear/tdl/core/storage"
"github.com/iyear/tdl/core/tclient"
"github.com/iyear/tdl/core/uploader"
"github.com/iyear/tdl/core/util/tutil"
"github.com/iyear/tdl/pkg/consts"
"github.com/iyear/tdl/pkg/prog"
"github.com/iyear/tdl/pkg/texpr"
"github.com/iyear/tdl/pkg/utils"
)
type Options struct {
Chat string
Thread int
To string
Paths []string
Includes []string
Excludes []string
Remove bool
Photo bool
Caption string
}
type Env struct {
FilePath string `comment:"File path"`
FileName string `comment:"File name"`
FileExt string `comment:"File extension"`
ThumbPath string `comment:"Thumbnail path"`
MIME string `comment:"File mime type"`
}
func Run(ctx context.Context, c *telegram.Client, kvd storage.Storage, opts Options) (rerr error) {
if opts.To == "-" || opts.Caption == "-" {
fg := texpr.NewFieldsGetter(nil)
fields, err := fg.Walk(exprEnv(context.Background(), nil))
if err != nil {
return fmt.Errorf("failed to walk fields: %w", err)
}
fmt.Print(fg.Sprint(fields, true))
return nil
}
files, err := walk(opts.Paths, opts.Includes, opts.Excludes)
if err != nil {
return err
}
color.Blue("Files count: %d", len(files))
pool := dcpool.NewPool(c,
int64(viper.GetInt(consts.FlagPoolSize)),
tclient.NewDefaultMiddlewares(ctx, viper.GetDuration(consts.FlagReconnectTimeout))...)
defer multierr.AppendInvoke(&rerr, multierr.Close(pool))
manager := peers.Options{Storage: storage.NewPeers(kvd)}.Build(pool.Default(ctx))
to, err := resolveDest(ctx, manager, opts.To)
if err != nil {
return errors.Wrap(err, "get target peer")
}
caption, err := resolveCaption(ctx, opts.Caption)
if err != nil {
return errors.Wrap(err, "get caption")
}
upProgress := prog.New(utils.Byte.FormatBinaryBytes)
upProgress.SetNumTrackersExpected(len(files))
prog.EnablePS(ctx, upProgress)
options := uploader.Options{
Client: pool.Default(ctx),
Threads: viper.GetInt(consts.FlagThreads),
Iter: newIter(files, to, caption, opts.Chat, opts.Thread, opts.Photo, opts.Remove, viper.GetDuration(consts.FlagDelay), manager),
Progress: newProgress(upProgress),
}
up := uploader.New(options)
go upProgress.Render()
defer prog.Wait(ctx, upProgress)
return up.Upload(ctx, viper.GetInt(consts.FlagLimit))
}
func resolveDest(ctx context.Context, manager *peers.Manager, input string) (*vm.Program, error) {
compile := func(i string) (*vm.Program, error) {
return expr.Compile(i, expr.Env(exprEnv(ctx, nil)))
}
if input == "" {
return compile(`""`)
}
if exp, err := os.ReadFile(input); err == nil {
return compile(string(exp))
}
if _, err := tutil.GetInputPeer(ctx, manager, input); err == nil {
return compile(fmt.Sprintf(`"%s"`, input))
}
return compile(input)
}
func resolveCaption(ctx context.Context, input string) (*vm.Program, error) {
compile := func(i string) (*vm.Program, error) {
// we pass empty peer and message to enable type checking
return expr.Compile(i, expr.Env(exprEnv(ctx, nil)), expr.AsKind(reflect.String))
}
// default
if input == "" {
return compile(`""`)
}
// file
if exp, err := os.ReadFile(input); err == nil {
return compile(string(exp))
}
// text
return compile(input)
}
func exprEnv(ctx context.Context, file *File) Env {
if file == nil {
return Env{}
}
extension := filepath.Ext(file.File)
filename := strings.TrimSuffix(filepath.Base(file.File), extension)
mime, err := mimetype.DetectFile(file.File)
if err != nil {
mime = &mimetype.MIME{}
logctx.From(ctx).Error("detect file mime", zap.Error(err))
}
return Env{
FilePath: file.File,
FileName: filename,
FileExt: extension,
ThumbPath: file.Thumb,
MIME: mime.String(),
}
}
================================================
FILE: app/up/walk.go
================================================
package up
import (
"io/fs"
"path/filepath"
"strings"
"github.com/iyear/tdl/core/util/fsutil"
"github.com/iyear/tdl/pkg/consts"
"github.com/iyear/tdl/pkg/filterMap"
)
func walk(paths, includes, excludes []string) ([]*File, error) {
files := make([]*File, 0)
includesMap := filterMap.New(includes, fsutil.AddPrefixDot)
excludesMap := filterMap.New(excludes, fsutil.AddPrefixDot)
excludesMap[consts.UploadThumbExt] = struct{}{} // ignore thumbnail files
for _, path := range paths {
err := filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
// process include and exclude
ext := filepath.Ext(path)
if _, ok := includesMap[ext]; len(includesMap) > 0 && !ok {
return nil
}
if _, ok := excludesMap[ext]; len(excludesMap) > 0 && ok {
return nil
}
f := File{File: path}
t := strings.TrimRight(path, filepath.Ext(path)) + consts.UploadThumbExt
if fsutil.PathExists(t) {
f.Thumb = t
}
files = append(files, &f)
return nil
})
if err != nil {
return nil, err
}
}
return files, nil
}
================================================
FILE: cmd/chat.go
================================================
package cmd
import (
"context"
"fmt"
"math"
"strings"
"time"
"github.com/gotd/contrib/middleware/ratelimit"
"github.com/gotd/td/telegram"
"github.com/spf13/cobra"
"golang.org/x/time/rate"
"github.com/iyear/tdl/app/chat"
"github.com/iyear/tdl/core/logctx"
"github.com/iyear/tdl/core/storage"
)
var limiter = ratelimit.New(rate.Every(500*time.Millisecond), 2)
func NewChat() *cobra.Command {
cmd := &cobra.Command{
Use: "chat",
Short: "A set of chat tools",
GroupID: groupTools.ID,
}
cmd.AddCommand(NewChatList(), NewChatExport(), NewChatUsers())
return cmd
}
func NewChatList() *cobra.Command {
var opts chat.ListOptions
cmd := &cobra.Command{
Use: "ls",
Short: "List your chats",
RunE: func(cmd *cobra.Command, args []string) error {
return tRun(cmd.Context(), func(ctx context.Context, c *telegram.Client, kvd storage.Storage) error {
return chat.List(logctx.Named(ctx, "ls"), c, kvd, opts)
}, limiter)
},
}
cmd.Flags().VarP(&opts.Output, "output", "o", fmt.Sprintf("output format: [%s]", strings.Join(chat.ListOutputNames(), ", ")))
cmd.Flags().StringVarP(&opts.Filter, "filter", "f", "true", "filter chats by expression")
return cmd
}
func NewChatExport() *cobra.Command {
var opts chat.ExportOptions
cmd := &cobra.Command{
Use: "export",
Short: "export messages from (protected) chat for download",
RunE: func(cmd *cobra.Command, args []string) error {
switch opts.Type {
case chat.ExportTypeTime, chat.ExportTypeId:
// set default value
switch len(opts.Input) {
case 0:
opts.Input = []int{0, math.MaxInt}
case 1:
opts.Input = append(opts.Input, math.MaxInt)
}
if len(opts.Input) != 2 {
return fmt.Errorf("input data should be 2 integers when export type is %s", opts.Type)
}
// sort helper
if opts.Input[0] > opts.Input[1] {
opts.Input[0], opts.Input[1] = opts.Input[1], opts.Input[0]
}
case chat.ExportTypeLast:
if len(opts.Input) != 1 {
return fmt.Errorf("input data should be 1 integer when export type is %s", opts.Type)
}
default:
return fmt.Errorf("unknown export type: %s", opts.Type)
}
return tRun(cmd.Context(), func(ctx context.Context, c *telegram.Client, kvd storage.Storage) error {
return chat.Export(logctx.Named(ctx, "export"), c, kvd, opts)
}, limiter)
},
}
const (
_type = "type"
_chat = "chat"
input = "input"
)
cmd.Flags().VarP(&opts.Type, _type, "T", fmt.Sprintf("export type: [%s]", strings.Join(chat.ExportTypeNames(), ", ")))
cmd.Flags().StringVarP(&opts.Chat, _chat, "c", "", "chat id or domain. If not specified, 'Saved Messages' will be used")
// topic id and message id is the same field in tg.MessagesGetRepliesRequest
cmd.Flags().IntVar(&opts.Thread, "topic", 0, "specify topic id")
cmd.Flags().IntVar(&opts.Thread, "reply", 0, "specify channel post id")
cmd.Flags().IntSliceVarP(&opts.Input, input, "i", []int{}, "input data, depends on export type")
cmd.Flags().StringVarP(&opts.Filter, "filter", "f", "true", "filter messages by expression, defaults to match all messages. Specify '-' to see available fields")
cmd.Flags().StringVarP(&opts.Output, "output", "o", "tdl-export.json", "output JSON file path")
cmd.Flags().BoolVar(&opts.WithContent, "with-content", false, "export with message content")
cmd.Flags().BoolVar(&opts.Raw, "raw", false, "export raw message struct of Telegram MTProto API, useful for debugging")
cmd.Flags().BoolVar(&opts.All, "all", false, "export all messages including non-media messages, but still affected by filter and type flag")
// completion and validation
_ = cmd.RegisterFlagCompletionFunc(input, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// if user has already input something, don't do anything
if toComplete != "" {
return []string{}, cobra.ShellCompDirectiveNoFileComp
}
switch cmd.Flags().Lookup(_type).Value.String() {
case chat.ExportTypeTime.String():
return []string{"0,9999999"}, cobra.ShellCompDirectiveNoFileComp
case chat.ExportTypeId.String():
return []string{"0,9999999"}, cobra.ShellCompDirectiveNoFileComp
case chat.ExportTypeLast.String():
return []string{"100"}, cobra.ShellCompDirectiveNoFileComp
default:
return []string{}, cobra.ShellCompDirectiveNoFileComp
}
})
return cmd
}
func NewChatUsers() *cobra.Command {
var opts chat.UsersOptions
cmd := &cobra.Command{
Use: "users",
Short: "export users from (protected) channels",
RunE: func(cmd *cobra.Command, args []string) error {
return tRun(cmd.Context(), func(ctx context.Context, c *telegram.Client, kvd storage.Storage) error {
return chat.Users(logctx.Named(ctx, "users"), c, kvd, opts)
}, limiter)
},
}
cmd.Flags().StringVarP(&opts.Output, "output", "o", "tdl-users.json", "output JSON file path")
cmd.Flags().StringVarP(&opts.Chat, "chat", "c", "", "domain id (channels, supergroups, etc.)")
cmd.Flags().BoolVar(&opts.Raw, "raw", false, "export raw message struct of Telegram MTProto API, useful for debugging")
return cmd
}
================================================
FILE: cmd/dl.go
================================================
package cmd
import (
"context"
"fmt"
"github.com/gotd/td/telegram"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/iyear/tdl/app/dl"
"github.com/iyear/tdl/core/logctx"
"github.com/iyear/tdl/core/storage"
"github.com/iyear/tdl/pkg/consts"
)
func NewDownload() *cobra.Command {
var opts dl.Options
cmd := &cobra.Command{
Use: "download",
Aliases: []string{"dl"},
Short: "Download anything from Telegram (protected) chat",
GroupID: groupTools.ID,
RunE: func(cmd *cobra.Command, args []string) error {
if len(opts.URLs) == 0 && len(opts.Files) == 0 {
return fmt.Errorf("no urls or files provided")
}
opts.Template = viper.GetString(consts.FlagDlTemplate)
return tRun(cmd.Context(), func(ctx context.Context, c *telegram.Client, kvd storage.Storage) error {
return dl.Run(logctx.Named(ctx, "dl"), c, kvd, opts)
})
},
}
const (
file = "file"
dir = "dir"
include = "include"
exclude = "exclude"
_continue = "continue"
restart = "restart"
)
cmd.Flags().StringSliceVarP(&opts.URLs, "url", "u", []string{}, "telegram message links")
cmd.Flags().StringSliceVarP(&opts.Files, file, "f", []string{}, "official client exported files")
cmd.Flags().String(consts.FlagDlTemplate, `{{ .DialogID }}_{{ .MessageID }}_{{ filenamify .FileName }}`, "download file name template")
cmd.Flags().StringSliceVarP(&opts.Include, include, "i", []string{}, "include the specified file extensions, and only judge by file name, not file MIME. Example: -i mp4,mp3")
cmd.Flags().StringSliceVarP(&opts.Exclude, exclude, "e", []string{}, "exclude the specified file extensions, and only judge by file name, not file MIME. Example: -e png,jpg")
cmd.Flags().StringVarP(&opts.Dir, dir, "d", "downloads", "specify the download directory. If the directory does not exist, it will be created automatically")
cmd.Flags().BoolVar(&opts.RewriteExt, "rewrite-ext", false, "rewrite file extension according to file header MIME")
// do not match extension, because some files' extension is corrected by --rewrite-ext flag
cmd.Flags().BoolVar(&opts.SkipSame, "skip-same", false, "skip files with the same name(without extension) and size")
cmd.Flags().BoolVar(&opts.Desc, "desc", false, "download files from the newest to the oldest ones (may affect resume download)")
cmd.Flags().BoolVar(&opts.Takeout, "takeout", false, "takeout sessions let you export data from your account with lower flood wait limits.")
cmd.Flags().BoolVar(&opts.Group, "group", false, "auto detect grouped message and download all of them")
// resume flags, if both false then ask user
cmd.Flags().BoolVar(&opts.Continue, _continue, false, "continue the last download directly")
cmd.Flags().BoolVar(&opts.Restart, restart, false, "restart the last download directly")
// serve flags
cmd.Flags().BoolVar(&opts.Serve, "serve", false, "serve the media files as a http server instead of downloading them with built-in downloader")
cmd.Flags().IntVar(&opts.Port, "port", 8080, "http server port")
_ = viper.BindPFlag(consts.FlagDlTemplate, cmd.Flags().Lookup(consts.FlagDlTemplate))
// completion and validation
_ = cmd.RegisterFlagCompletionFunc(file, completeExtFiles("json"))
_ = cmd.MarkFlagDirname(dir)
cmd.MarkFlagsMutuallyExclusive(include, exclude)
cmd.MarkFlagsMutuallyExclusive(_continue, restart)
return cmd
}
================================================
FILE: cmd/extension.go
================================================
package cmd
import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"github.com/go-faster/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/iyear/tdl/app/extension"
"github.com/iyear/tdl/core/storage"
extbase "github.com/iyear/tdl/extension"
"github.com/iyear/tdl/pkg/consts"
"github.com/iyear/tdl/pkg/extensions"
"github.com/iyear/tdl/pkg/tclient"
)
func NewExtension(em *extensions.Manager) *cobra.Command {
var dryRun bool
cmd := &cobra.Command{
Use: "extension",
Short: "Manage tdl extensions",
GroupID: groupTools.ID,
Aliases: []string{"extensions", "ext"},
PersistentPreRun: func(cmd *cobra.Command, args []string) {
em.SetDryRun(dryRun)
},
}
cmd.AddCommand(NewExtensionList(em), NewExtensionInstall(em), NewExtensionRemove(em), NewExtensionUpgrade(em))
cmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "only print what would be done without actually doing it")
return cmd
}
func NewExtensionList(em *extensions.Manager) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List installed extension commands",
Aliases: []string{"ls"},
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return extension.List(cmd.Context(), em)
},
}
return cmd
}
func NewExtensionInstall(em *extensions.Manager) *cobra.Command {
var force bool
cmd := &cobra.Command{
Use: "install",
Short: "Install a tdl extension",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return extension.Install(cmd.Context(), em, args, force)
},
}
cmd.Flags().BoolVar(&force, "force", false, "force install even if extension already exists")
return cmd
}
func NewExtensionUpgrade(em *extensions.Manager) *cobra.Command {
cmd := &cobra.Command{
Use: "upgrade",
Short: "Upgrade a tdl extension",
RunE: func(cmd *cobra.Command, args []string) error {
return extension.Upgrade(cmd.Context(), em, args)
},
}
return cmd
}
func NewExtensionRemove(em *extensions.Manager) *cobra.Command {
cmd := &cobra.Command{
Use: "remove",
Short: "Remove an installed extension",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return extension.Remove(cmd.Context(), em, args)
},
}
return cmd
}
func NewExtensionCmd(em *extensions.Manager, ext extensions.Extension, stdin io.Reader, stdout, stderr io.Writer) *cobra.Command {
return &cobra.Command{
Use: ext.Name(),
Short: fmt.Sprintf("Extension %s", ext.Name()),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
opts, err := tOptions(ctx)
if err != nil {
return errors.Wrap(err, "build telegram options")
}
app, err := tclient.GetApp(opts.KV)
if err != nil {
return errors.Wrap(err, "get app")
}
session, err := storage.NewSession(opts.KV, false).LoadSession(ctx)
if err != nil {
return errors.Wrap(err, "load session")
}
dataDir := filepath.Join(consts.ExtensionsDataPath, ext.Name())
if err = os.MkdirAll(dataDir, 0o755); err != nil {
return errors.Wrap(err, "create extension data dir")
}
env := &extbase.Env{
Name: ext.Name(),
AppID: app.AppID,
AppHash: app.AppHash,
Session: session,
Namespace: viper.GetString(consts.FlagNamespace),
DataDir: dataDir,
NTP: opts.NTP,
Proxy: opts.Proxy,
Pool: viper.GetInt64(consts.FlagPoolSize),
Debug: viper.GetBool(consts.FlagDebug),
}
if err = em.Dispatch(ext, args, env, stdin, stdout, stderr); err != nil {
var execError *exec.ExitError
if errors.As(err, &execError) {
return execError
}
return fmt.Errorf("failed to run extension: %w", err)
}
return nil
},
GroupID: groupExtensions.ID,
DisableFlagParsing: true,
}
}
================================================
FILE: cmd/forward.go
================================================
package cmd
import (
"context"
"fmt"
"strings"
"github.com/gotd/td/telegram"
"github.com/spf13/cobra"
"github.com/iyear/tdl/app/forward"
"github.com/iyear/tdl/core/forwarder"
"github.com/iyear/tdl/core/logctx"
"github.com/iyear/tdl/core/storage"
)
func NewForward() *cobra.Command {
var opts forward.Options
cmd := &cobra.Command{
Use: "forward",
Short: "Forward messages with automatic fallback and message routing",
GroupID: groupTools.ID,
RunE: func(cmd *cobra.Command, args []string) error {
return tRun(cmd.Context(), func(ctx context.Context, c *telegram.Client, kvd storage.Storage) error {
return forward.Run(logctx.Named(ctx, "forward"), c, kvd, opts)
})
},
}
cmd.Flags().StringArrayVar(&opts.From, "from", []string{}, "messages to be forwarded, can be links or exported JSON files")
cmd.Flags().StringVar(&opts.To, "to", "", "destination peer, can be a CHAT or router based on expression engine")
cmd.Flags().StringVar(&opts.Edit, "edit", "", "edit message or caption with expression engine. Empty means no edit")
cmd.Flags().Var(&opts.Mode, "mode", fmt.Sprintf("forward mode: [%s]", strings.Join(forwarder.ModeNames(), ", ")))
cmd.Flags().BoolVar(&opts.Silent, "silent", false, "send messages silently")
cmd.Flags().BoolVar(&opts.DryRun, "dry-run", false, "do not actually send messages, just show how they would be sent")
cmd.Flags().BoolVar(&opts.Single, "single", false, "do not automatically detect and forward grouped messages")
cmd.Flags().BoolVar(&opts.Desc, "desc", false, "forward messages in reverse order for each input peer")
return cmd
}
================================================
FILE: cmd/gen.go
================================================
package cmd
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"github.com/go-faster/errors"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
"github.com/iyear/tdl/core/util/fsutil"
)
func NewGen() *cobra.Command {
cmd := &cobra.Command{
Use: "gen",
Short: "A set of gen tools",
Hidden: true,
}
cmd.AddCommand(NewGenDoc())
return cmd
}
func NewGenDoc() *cobra.Command {
var dir string
cmd := &cobra.Command{
Use: "doc",
Short: "Generate doc",
RunE: func(cmd *cobra.Command, args []string) error {
const frontmatter = `---
title: "%s"
bookHidden: true
---
`
cmd.VisitParents(func(c *cobra.Command) {
// Disable the "Auto generated by spf13/cobra on DATE"
// as it creates a lot of diffs.
c.DisableAutoGenTag = true
})
if !fsutil.PathExists(dir) {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return errors.Wrap(err, "mkdir")
}
}
prepender := func(filename string) string {
name := filepath.Base(filename)
base := strings.TrimSuffix(name, path.Ext(name))
return fmt.Sprintf(frontmatter, strings.ReplaceAll(base, "_", " "))
}
linkHandler := func(name string) string {
base := strings.TrimSuffix(name, path.Ext(name))
return "/more/cli/" + strings.ToLower(base) + "/"
}
fmt.Println("Generating command-line documentation in", dir, "...")
err := doc.GenMarkdownTreeCustom(cmd.Root(), dir, prepender, linkHandler)
if err != nil {
return errors.Wrap(err, "gendoc")
}
fmt.Println("Done.")
return nil
},
}
cmd.Flags().StringVarP(&dir, "dir", "d", "", "dir to generate doc")
_ = cmd.MarkFlagRequired("dir")
return cmd
}
================================================
FILE: cmd/login.go
================================================
package cmd
import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/iyear/tdl/app/login"
"github.com/iyear/tdl/core/logctx"
)
func NewLogin() *cobra.Command {
var (
code bool
opts login.Options
)
cmd := &cobra.Command{
Use: "login",
Short: "Login to Telegram",
GroupID: groupAccount.ID,
RunE: func(cmd *cobra.Command, args []string) error {
color.Yellow("WARN: If data exists in the namespace, data will be overwritten")
// Legacy flag
if code {
return login.Code(logctx.Named(cmd.Context(), "login"))
}
return login.Run(logctx.Named(cmd.Context(), "login"), opts)
},
}
const desktop = "desktop"
cmd.Flags().VarP(&opts.Type, "type", "T", fmt.Sprintf("login mode: [%s]", strings.Join(login.TypeNames(), ", ")))
cmd.Flags().StringVarP(&opts.Desktop, desktop, "d", "", "official desktop client path, and automatically find possible paths if empty")
cmd.Flags().StringVarP(&opts.Passcode, "passcode", "p", "", "passcode for desktop client, keep empty if no passcode")
// Deprecated
cmd.Flags().BoolVar(&code, "code", false, "login with code, instead of importing session from desktop client")
// completion and validation
_ = cmd.MarkFlagDirname(desktop)
_ = cmd.Flags().MarkDeprecated("code", "use `-T code` instead")
return cmd
}
================================================
FILE: cmd/migrate.go
================================================
package cmd
import (
"fmt"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/iyear/tdl/app/migrate"
"github.com/iyear/tdl/pkg/kv"
)
func NewBackup() *cobra.Command {
var dst string
cmd := &cobra.Command{
Use: "backup",
Short: "Backup your data",
GroupID: groupAccount.ID,
RunE: func(cmd *cobra.Command, args []string) error {
if dst == "" {
dst = fmt.Sprintf("%s.backup.tdl", time.Now().Format("2006-01-02-15_04_05"))
}
return migrate.Backup(cmd.Context(), dst)
},
}
cmd.Flags().StringVarP(&dst, "dst", "d", "", "destination file path. Default: <date>.backup.tdl")
return cmd
}
func NewRecover() *cobra.Command {
var file string
cmd := &cobra.Command{
Use: "recover",
Short: "Recover your data",
GroupID: groupAccount.ID,
RunE: func(cmd *cobra.Command, args []string) error {
return migrate.Recover(cmd.Context(), file)
},
}
const fileFlag = "file"
cmd.Flags().StringVarP(&file, fileFlag, "f", "", "backup file path")
// completion and validation
_ = cmd.RegisterFlagCompletionFunc(fileFlag, completeExtFiles("tdl"))
_ = cmd.MarkFlagRequired(fileFlag)
return cmd
}
func NewMigrate() *cobra.Command {
var to map[string]string
cmd := &cobra.Command{
Use: "migrate",
Short: "Migrate your current data to another storage",
GroupID: groupAccount.ID,
RunE: func(cmd *cobra.Command, args []string) error {
return migrate.Migrate(cmd.Context(), to)
},
}
cmd.Flags().StringToStringVar(&to, "to", map[string]string{},
fmt.Sprintf("destination storage options, format: type=driver,key1=value1,key2=value2. Available drivers: [%s]",
strings.Join(kv.DriverNames(), ",")))
return cmd
}
================================================
FILE: cmd/root.go
================================================
package cmd
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/go-faster/errors"
"github.com/gotd/td/telegram"
"github.com/ivanpirog/coloredcobra"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/multierr"
"go.uber.org/zap"
"golang.org/x/net/proxy"
"github.com/iyear/tdl/core/logctx"
"github.com/iyear/tdl/core/storage"
tclientcore "github.com/iyear/tdl/core/tclient"
"github.com/iyear/tdl/core/util/fsutil"
"github.com/iyear/tdl/core/util/logutil"
"github.com/iyear/tdl/core/util/netutil"
"github.com/iyear/tdl/pkg/consts"
"github.com/iyear/tdl/pkg/extensions"
"github.com/iyear/tdl/pkg/kv"
"github.com/iyear/tdl/pkg/tclient"
)
var (
defaultBoltPath = filepath.Join(consts.DataDir, "data")
DefaultLegacyStorage = map[string]string{
kv.DriverTypeKey: kv.DriverLegacy.String(),
"path": filepath.Join(consts.DataDir, "data.kv"),
}
DefaultBoltStorage = map[string]string{
kv.DriverTypeKey: kv.DriverBolt.String(),
"path": defaultBoltPath,
}
)
// command groups
var (
groupAccount = &cobra.Group{
ID: "account",
Title: "Account related",
}
groupTools = &cobra.Group{
ID: "tools",
Title: "Tools",
}
groupExtensions = &cobra.Group{
ID: "extensions",
Title: "Extensions",
}
)
func New() *cobra.Command {
// allow PersistentPreRun to be called for every command
cobra.EnableTraverseRunHooks = true
em := extensions.NewManager(consts.ExtensionsPath)
cmd := &cobra.Command{
Use: "tdl",
Short: "Telegram Downloader, but more than a downloader",
SilenceErrors: true,
SilenceUsage: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// init logger
debug, level := viper.GetBool(consts.FlagDebug), zap.InfoLevel
if debug {
level = zap.DebugLevel
}
cmd.SetContext(logctx.With(cmd.Context(),
logutil.New(level, filepath.Join(consts.LogPath, "latest.log"))))
ns := viper.GetString(consts.FlagNamespace)
if ns != "" {
logctx.From(cmd.Context()).Info("Namespace",
zap.String("namespace", ns))
}
// v0.14.0: default storage changed from legacy to bolt, so we need to auto migrate to keep compatibility
if !cmd.Flags().Lookup(consts.FlagStorage).Changed && !fsutil.PathExists(defaultBoltPath) {
if err := migrateLegacyToBolt(); err != nil {
return errors.Wrap(err, "migrate legacy to bolt")
}
}
stg, err := kv.NewWithMap(viper.GetStringMapString(consts.FlagStorage))
if err != nil {
return errors.Wrap(err, "create kv storage")
}
cmd.SetContext(kv.With(cmd.Context(), stg))
// extension manager client proxy
var dialer proxy.ContextDialer = proxy.Direct
if p := viper.GetString(consts.FlagProxy); p != "" {
if t, err := netutil.NewProxy(p); err == nil {
dialer = t
}
}
em.SetClient(&http.Client{Transport: &http.Transport{
DialContext: dialer.DialContext,
}})
return nil
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
return multierr.Combine(
kv.From(cmd.Context()).Close(),
logctx.From(cmd.Context()).Sync(),
)
},
}
coloredcobra.Init(&coloredcobra.Config{
RootCmd: cmd,
Headings: coloredcobra.HiCyan + coloredcobra.Bold + coloredcobra.Underline,
Commands: coloredcobra.HiGreen + coloredcobra.Bold,
CmdShortDescr: coloredcobra.None,
ExecName: coloredcobra.Bold,
Flags: coloredcobra.Bold + coloredcobra.Yellow,
FlagsDataType: coloredcobra.Blue,
FlagsDescr: coloredcobra.None,
Aliases: coloredcobra.None,
Example: coloredcobra.None,
NoExtraNewlines: true,
NoBottomNewline: true,
})
cmd.AddGroup(groupAccount, groupTools, groupExtensions)
cmd.AddCommand(NewVersion(), NewLogin(), NewDownload(), NewForward(),
NewChat(), NewUpload(), NewBackup(), NewRecover(), NewMigrate(),
NewGen(), NewExtension(em))
// append extension command to root
exts, _ := em.List(context.Background(), false)
for _, e := range exts {
cmd.AddCommand(NewExtensionCmd(em, e, os.Stdin, os.Stdout, os.Stderr))
}
cmd.PersistentFlags().StringToString(consts.FlagStorage,
DefaultBoltStorage,
fmt.Sprintf("storage options, format: type=driver,key1=value1,key2=value2. Available drivers: [%s]",
strings.Join(kv.DriverNames(), ",")))
cmd.PersistentFlags().String(consts.FlagProxy, "", "proxy address, format: protocol://username:password@host:port")
cmd.PersistentFlags().StringP(consts.FlagNamespace, "n", "default", "namespace for Telegram session")
cmd.PersistentFlags().Bool(consts.FlagDebug, false, "enable debug mode")
cmd.PersistentFlags().IntP(consts.FlagPartSize, "s", 512*1024, "part size for transfer")
_ = cmd.PersistentFlags().MarkDeprecated(consts.FlagPartSize, "part size has been set to maximum by default, this flag will be removed in the future")
cmd.PersistentFlags().IntP(consts.FlagThreads, "t", 4, "max threads for transfer one item")
cmd.PersistentFlags().IntP(consts.FlagLimit, "l", 2, "max number of concurrent tasks")
cmd.PersistentFlags().Int(consts.FlagPoolSize, 8, "specify the size of the DC pool, zero means infinity")
cmd.PersistentFlags().Duration(consts.FlagDelay, 0, "delay between each task, zero means no delay")
cmd.PersistentFlags().String(consts.FlagNTP, "", "ntp server host, if not set, use system time")
cmd.PersistentFlags().Duration(consts.FlagReconnectTimeout, 5*time.Minute, "Telegram client reconnection backoff timeout, infinite if set to 0") // #158
// completion
_ = cmd.RegisterFlagCompletionFunc(consts.FlagNamespace, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
engine := kv.From(cmd.Context())
ns, err := engine.Namespaces()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return ns, cobra.ShellCompDirectiveNoFileComp
})
_ = viper.BindPFlags(cmd.PersistentFlags())
viper.SetEnvPrefix("tdl")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.AutomaticEnv()
// extension command format: <global-flags> <extension-name> <extension-flags>,
// which means parse args layer by layer. But common command flags are flat.
// To keep compatibility, we only set TraverseChildren to true for extension
// command instead of other commands.
foundCmd, _, err := cmd.Find(os.Args[1:])
if err == nil && foundCmd.GroupID == groupExtensions.ID {
cmd.TraverseChildren = true // allow global config to be parsed before extension command is executed
}
return cmd
}
type completeFunc func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
func completeExtFiles(ext ...string) completeFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
files := make([]string, 0)
for _, e := range ext {
f, err := filepath.Glob(toComplete + "*." + e)
if err != nil {
return nil, cobra.ShellCompDirectiveDefault
}
files = append(files, f...)
}
return files, cobra.ShellCompDirectiveFilterDirs
}
}
func tOptions(ctx context.Context) (tclient.Options, error) {
// init tclient kv
kvd, err := kv.From(ctx).Open(viper.GetString(consts.FlagNamespace))
if err != nil {
return tclient.Options{}, errors.Wrap(err, "open kv storage")
}
o := tclient.Options{
KV: kvd,
Proxy: viper.GetString(consts.FlagProxy),
NTP: viper.GetString(consts.FlagNTP),
ReconnectTimeout: viper.GetDuration(consts.FlagReconnectTimeout),
UpdateHandler: nil,
}
return o, nil
}
func tRun(ctx context.Context, f func(ctx context.Context, c *telegram.Client, kvd storage.Storage) error, middlewares ...telegram.Middleware) error {
o, err := tOptions(ctx)
if err != nil {
return errors.Wrap(err, "build telegram options")
}
client, err := tclient.New(ctx, o, false, middlewares...)
if err != nil {
return errors.Wrap(err, "create client")
}
return tclientcore.RunWithAuth(ctx, client, func(ctx context.Context) error {
return f(ctx, client, o.KV)
})
}
func migrateLegacyToBolt() (rerr error) {
legacy, err := kv.NewWithMap(DefaultLegacyStorage)
if err != nil {
return errors.Wrap(err, "create legacy kv storage")
}
defer multierr.AppendInvoke(&rerr, multierr.Close(legacy))
bolt, err := kv.NewWithMap(DefaultBoltStorage)
if err != nil {
return errors.Wrap(err, "create bolt kv storage")
}
defer multierr.AppendInvoke(&rerr, multierr.Close(bolt))
meta, err := legacy.MigrateTo()
if err != nil {
return errors.Wrap(err, "migrate legacy to bolt")
}
return bolt.MigrateFrom(meta)
}
================================================
FILE: cmd/up.go
================================================
package cmd
import (
"context"
"errors"
"github.com/gotd/td/telegram"
"github.com/spf13/cobra"
"github.com/iyear/tdl/app/up"
"github.com/iyear/tdl/core/logctx"
"github.com/iyear/tdl/core/storage"
)
func NewUpload() *cobra.Command {
var opts up.Options
cmd := &cobra.Command{
Use: "upload",
Aliases: []string{"up"},
Short: "Upload anything to Telegram",
GroupID: groupTools.ID,
RunE: func(cmd *cobra.Command, args []string) error {
return tRun(cmd.Context(), func(ctx context.Context, c *telegram.Client, kvd storage.Storage) error {
if opts.Thread != 0 && opts.Chat == "" {
return errors.New("error flags: --chat should be set when --topic is set")
}
if opts.Chat != "" && opts.To != "" {
return errors.New("conflicting flags: --chat and --to cannot be set at the same time")
}
return up.Run(logctx.Named(ctx, "up"), c, kvd, opts)
})
},
}
const (
_chat = "chat"
path = "path"
include = "include"
exclude = "exclude"
)
cmd.Flags().StringVarP(&opts.Chat, _chat, "c", "", "chat id or domain, and empty means 'Saved Messages'. Can be used together with --topic flag. Conflicts with --to flag.")
cmd.Flags().IntVar(&opts.Thread, "topic", 0, "specify topic id. Must be used together with --chat flag. Conflicts with --to flag.")
cmd.Flags().StringVar(&opts.To, "to", "", "destination peer, can be a CHAT or router based on expression engine. Conflicts with --chat and --topic flag.")
cmd.Flags().StringSliceVarP(&opts.Paths, path, "p", []string{}, "dirs or files")
cmd.Flags().StringSliceVarP(&opts.Includes, include, "i", []string{}, "include the specified file extensions")
cmd.Flags().StringSliceVarP(&opts.Excludes, exclude, "e", []string{}, "exclude the specified file extensions")
cmd.Flags().BoolVar(&opts.Remove, "rm", false, "remove the uploaded files after uploading")
cmd.Flags().BoolVar(&opts.Photo, "photo", false, "upload the image as a photo instead of a file")
cmd.Flags().StringVar(&opts.Caption, "caption", `"<code>"+FileName+"</code> - <code>"+MIME+"</code>"`, "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:
gitextract_qifrln_6/
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yaml
│ │ └── feature_request.yml
│ ├── dependabot.yml
│ └── workflows/
│ ├── dependabot-fix.yml
│ ├── docker.yml
│ ├── docs.yml
│ ├── master.yml
│ └── release.yml
├── .gitignore
├── .golangci.yaml
├── .goreleaser.yaml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── README_zh.md
├── app/
│ ├── chat/
│ │ ├── export.go
│ │ ├── export_enum.go
│ │ ├── ls.go
│ │ ├── ls_enum.go
│ │ └── users.go
│ ├── dl/
│ │ ├── dl.go
│ │ ├── elem.go
│ │ ├── iter.go
│ │ ├── iter_test.go
│ │ ├── progress.go
│ │ ├── serve.go
│ │ └── serve.go.tmpl
│ ├── extension/
│ │ └── extension.go
│ ├── forward/
│ │ ├── elem.go
│ │ ├── forward.go
│ │ ├── iter.go
│ │ └── progress.go
│ ├── internal/
│ │ └── tctx/
│ │ └── tctx.go
│ ├── login/
│ │ ├── code.go
│ │ ├── desktop.go
│ │ ├── login.go
│ │ ├── login_enum.go
│ │ └── qr.go
│ ├── migrate/
│ │ ├── backup.go
│ │ ├── migrate.go
│ │ └── recover.go
│ └── up/
│ ├── elem.go
│ ├── iter.go
│ ├── progress.go
│ ├── up.go
│ └── walk.go
├── cmd/
│ ├── chat.go
│ ├── dl.go
│ ├── extension.go
│ ├── forward.go
│ ├── gen.go
│ ├── login.go
│ ├── migrate.go
│ ├── root.go
│ ├── up.go
│ ├── version.go
│ └── version.tmpl
├── core/
│ ├── dcpool/
│ │ ├── dcpool.go
│ │ └── middlewares.go
│ ├── downloader/
│ │ ├── downloader.go
│ │ ├── iter.go
│ │ └── progress.go
│ ├── forwarder/
│ │ ├── clone.go
│ │ ├── forwarder.go
│ │ ├── forwarder_enum.go
│ │ ├── iter.go
│ │ └── progress.go
│ ├── go.mod
│ ├── go.sum
│ ├── logctx/
│ │ └── logctx.go
│ ├── middlewares/
│ │ ├── recovery/
│ │ │ └── recovery.go
│ │ ├── retry/
│ │ │ └── retry.go
│ │ └── takeout/
│ │ ├── middleware.go
│ │ └── takeout.go
│ ├── storage/
│ │ ├── keygen/
│ │ │ └── keygen.go
│ │ ├── peers.go
│ │ ├── session.go
│ │ ├── state.go
│ │ └── storage.go
│ ├── tclient/
│ │ └── tclient.go
│ ├── tmedia/
│ │ ├── convert.go
│ │ ├── document.go
│ │ ├── media.go
│ │ └── photo.go
│ ├── uploader/
│ │ ├── iter.go
│ │ ├── progress.go
│ │ └── uploader.go
│ └── util/
│ ├── fsutil/
│ │ └── fsutil.go
│ ├── logutil/
│ │ └── logutil.go
│ ├── mediautil/
│ │ └── mediautil.go
│ ├── netutil/
│ │ └── netutil.go
│ └── tutil/
│ ├── device.go
│ └── tutil.go
├── docs/
│ ├── assets/
│ │ └── _custom.scss
│ ├── content/
│ │ ├── en/
│ │ │ ├── _index.md
│ │ │ ├── getting-started/
│ │ │ │ ├── _index.md
│ │ │ │ ├── installation.md
│ │ │ │ ├── quick-start.md
│ │ │ │ └── shell-completion.md
│ │ │ ├── guide/
│ │ │ │ ├── _index.md
│ │ │ │ ├── download.md
│ │ │ │ ├── extensions.md
│ │ │ │ ├── forward.md
│ │ │ │ ├── global-config.md
│ │ │ │ ├── login.md
│ │ │ │ ├── migration.md
│ │ │ │ ├── template.md
│ │ │ │ ├── tools/
│ │ │ │ │ ├── _index.md
│ │ │ │ │ ├── export-members.md
│ │ │ │ │ ├── export-messages.md
│ │ │ │ │ └── list-chats.md
│ │ │ │ └── upload.md
│ │ │ ├── more/
│ │ │ │ ├── _index.md
│ │ │ │ ├── cli/
│ │ │ │ │ └── _index.md
│ │ │ │ ├── data.md
│ │ │ │ ├── env.md
│ │ │ │ └── troubleshooting.md
│ │ │ ├── reference/
│ │ │ │ ├── _index.md
│ │ │ │ └── expr.md
│ │ │ └── snippets/
│ │ │ ├── _index.md
│ │ │ ├── chat.md
│ │ │ └── link.md
│ │ └── zh/
│ │ ├── _index.md
│ │ ├── getting-started/
│ │ │ ├── _index.md
│ │ │ ├── installation.md
│ │ │ ├── quick-start.md
│ │ │ └── shell-completion.md
│ │ ├── guide/
│ │ │ ├── _index.md
│ │ │ ├── download.md
│ │ │ ├── extensions.md
│ │ │ ├── forward.md
│ │ │ ├── global-config.md
│ │ │ ├── login.md
│ │ │ ├── migration.md
│ │ │ ├── template.md
│ │ │ ├── tools/
│ │ │ │ ├── _index.md
│ │ │ │ ├── export-members.md
│ │ │ │ ├── export-messages.md
│ │ │ │ └── list-chats.md
│ │ │ └── upload.md
│ │ ├── more/
│ │ │ ├── _index.md
│ │ │ ├── cli/
│ │ │ │ └── _index.md
│ │ │ ├── data.md
│ │ │ ├── env.md
│ │ │ └── troubleshooting.md
│ │ ├── reference/
│ │ │ ├── _index.md
│ │ │ └── expr.md
│ │ └── snippets/
│ │ ├── _index.md
│ │ ├── chat.md
│ │ └── link.md
│ ├── go.mod
│ ├── go.sum
│ ├── hugo.yaml
│ ├── layouts/
│ │ ├── partials/
│ │ │ └── docs/
│ │ │ └── inject/
│ │ │ ├── footer.html
│ │ │ └── head.html
│ │ └── shortcodes/
│ │ ├── command.html
│ │ ├── image.html
│ │ └── include.html
│ └── resources/
│ └── _gen/
│ └── assets/
│ └── scss/
│ ├── book.scss_e129fe35b8d0a70789c8a08429469073.content
│ └── book.scss_e129fe35b8d0a70789c8a08429469073.json
├── extension/
│ ├── extension.go
│ ├── go.mod
│ └── go.sum
├── go.mod
├── go.sum
├── go.work
├── go.work.sum
├── hack/
│ ├── lib.sh
│ └── release_mod.sh
├── main.go
├── pkg/
│ ├── clock/
│ │ └── clock.go
│ ├── consts/
│ │ ├── consts.go
│ │ ├── flag.go
│ │ ├── path.go
│ │ └── version.go
│ ├── extensions/
│ │ ├── extensions.go
│ │ ├── extensions_enum.go
│ │ ├── extensions_test.go
│ │ ├── github.go
│ │ ├── local.go
│ │ ├── local_test.go
│ │ └── manager.go
│ ├── filterMap/
│ │ └── filterMap.go
│ ├── key/
│ │ └── key.go
│ ├── kv/
│ │ ├── bolt.go
│ │ ├── file.go
│ │ ├── kv.go
│ │ ├── kv_enum.go
│ │ ├── kv_test.go
│ │ └── legacy.go
│ ├── prog/
│ │ ├── prog.go
│ │ └── tracker.go
│ ├── ps/
│ │ └── ps.go
│ ├── tclient/
│ │ ├── app.go
│ │ └── tclient.go
│ ├── tdesktop/
│ │ ├── .s
│ │ └── tdesktop.go
│ ├── texpr/
│ │ ├── env.go
│ │ ├── env_test.go
│ │ ├── expr.go
│ │ ├── fields.go
│ │ └── fields_test.go
│ ├── tmessage/
│ │ ├── files.go
│ │ ├── tmessage.go
│ │ └── urls.go
│ ├── tpath/
│ │ ├── tpath.go
│ │ ├── tpath_darwin.go
│ │ ├── tpath_linux.go
│ │ ├── tpath_other.go
│ │ └── tpath_windows.go
│ ├── tplfunc/
│ │ ├── date.go
│ │ ├── date_test.go
│ │ ├── func.go
│ │ ├── math.go
│ │ ├── math_test.go
│ │ ├── string.go
│ │ └── string_test.go
│ ├── utils/
│ │ ├── byte.go
│ │ └── cmd.go
│ └── validator/
│ └── validator.go
├── scripts/
│ ├── install.ps1
│ └── install.sh
└── test/
├── archive_test.go
├── chat_ls_test.go
├── chat_users_test.go
├── download_test.go
├── suite_test.go
├── testserver/
│ ├── public_key.pem
│ └── testserver.go
└── upload_test.go
SYMBOL INDEX (695 symbols across 124 files)
FILE: app/chat/export.go
type ExportOptions (line 30) | type ExportOptions struct
type Message (line 43) | type Message struct
type ExportType (line 54) | type ExportType
function Export (line 56) | func Export(ctx context.Context, c *telegram.Client, kvd storage.Storage...
FILE: app/chat/export_enum.go
constant ExportTypeTime (line 16) | ExportTypeTime ExportType = iota
constant ExportTypeId (line 18) | ExportTypeId
constant ExportTypeLast (line 20) | ExportTypeLast
constant _ExportTypeName (line 25) | _ExportTypeName = "timeidlast"
function ExportTypeNames (line 34) | func ExportTypeNames() []string {
function ExportTypeValues (line 41) | func ExportTypeValues() []ExportType {
method String (line 56) | func (x ExportType) String() string {
method IsValid (line 65) | func (x ExportType) IsValid() bool {
function ParseExportType (line 80) | func ParseExportType(name string) (ExportType, error) {
method Set (line 92) | func (x *ExportType) Set(val string) error {
method Get (line 99) | func (x *ExportType) Get() interface{} {
method Type (line 104) | func (x *ExportType) Type() string {
FILE: app/chat/ls.go
type Dialog (line 29) | type Dialog struct
type Topic (line 37) | type Topic struct
type ListOutput (line 44) | type ListOutput
constant DialogGroup (line 48) | DialogGroup = "group"
constant DialogPrivate (line 49) | DialogPrivate = "private"
constant DialogChannel (line 50) | DialogChannel = "channel"
constant DialogUnknown (line 51) | DialogUnknown = "unknown"
type ListOptions (line 54) | type ListOptions struct
function List (line 59) | func List(ctx context.Context, c *telegram.Client, kvd storage.Storage, ...
function printTable (line 157) | func printTable(result []*Dialog) {
function trunc (line 175) | func trunc(s string, len int) string {
function topicsString (line 184) | func topicsString(topics []Topic) string {
function processUser (line 197) | func processUser(id int64, entities peer.Entities) *Dialog {
function processChannel (line 212) | func processChannel(ctx context.Context, api *tg.Client, id int64, entit...
function fetchTopics (line 251) | func fetchTopics(ctx context.Context, api *tg.Client, c tg.InputChannelC...
function processChat (line 335) | func processChat(id int64, entities peer.Entities) *Dialog {
function visibleName (line 350) | func visibleName(first, last string) string {
function applyPeers (line 366) | func applyPeers(ctx context.Context, manager *peers.Manager, entities pe...
function fetchDialogsWithErrorHandling (line 387) | func fetchDialogsWithErrorHandling(ctx context.Context, api *tg.Client) ...
FILE: app/chat/ls_enum.go
constant ListOutputTable (line 16) | ListOutputTable ListOutput = iota
constant ListOutputJson (line 18) | ListOutputJson
constant _ListOutputName (line 23) | _ListOutputName = "tablejson"
function ListOutputNames (line 31) | func ListOutputNames() []string {
function ListOutputValues (line 38) | func ListOutputValues() []ListOutput {
method String (line 51) | func (x ListOutput) String() string {
method IsValid (line 60) | func (x ListOutput) IsValid() bool {
function ParseListOutput (line 73) | func ParseListOutput(name string) (ListOutput, error) {
method Set (line 85) | func (x *ListOutput) Set(val string) error {
method Get (line 92) | func (x *ListOutput) Get() interface{} {
method Type (line 97) | func (x *ListOutput) Type() string {
FILE: app/chat/users.go
type UsersOptions (line 26) | type UsersOptions struct
type User (line 32) | type User struct
function Users (line 40) | func Users(ctx context.Context, c *telegram.Client, kvd storage.Storage,...
function outputUsers (line 109) | func outputUsers(ctx context.Context,
function convertTelegramUser (line 161) | func convertTelegramUser(u *tg.User) User {
FILE: app/dl/dl.go
type Options (line 29) | type Options struct
type parser (line 50) | type parser struct
function Run (line 55) | func Run(ctx context.Context, c *telegram.Client, kvd storage.Storage, o...
function collectDialogs (line 143) | func collectDialogs(parsers []parser) ([][]*tmessage.Dialog, error) {
function resume (line 155) | func resume(ctx context.Context, kvd storage.Storage, iter *iter, ask bo...
function saveProgress (line 202) | func saveProgress(ctx context.Context, kvd storage.Storage, it *iter) er...
FILE: app/dl/elem.go
type iterElem (line 14) | type iterElem struct
method File (line 27) | func (i *iterElem) File() downloader.File { return i }
method To (line 29) | func (i *iterElem) To() io.WriterAt { return i.to }
method AsTakeout (line 31) | func (i *iterElem) AsTakeout() bool { return i.opts.Takeout }
method Location (line 33) | func (i *iterElem) Location() tg.InputFileLocationClass { return i.fil...
method Name (line 35) | func (i *iterElem) Name() string { return i.file.Name }
method Size (line 37) | func (i *iterElem) Size() int64 { return i.file.Size }
method DC (line 39) | func (i *iterElem) DC() int { return i.file.DC }
FILE: app/dl/iter.go
constant tempExt (line 34) | tempExt = ".tmp"
type fileTemplate (line 36) | type fileTemplate struct
type iter (line 46) | type iter struct
method Next (line 122) | func (i *iter) Next(ctx context.Context) bool {
method process (line 149) | func (i *iter) process(ctx context.Context) (ret bool, skip bool) {
method processSingle (line 208) | func (i *iter) processSingle(ctx context.Context, message *tg.Message,...
method processGrouped (line 283) | func (i *iter) processGrouped(ctx context.Context, message *tg.Message...
method Value (line 319) | func (i *iter) Value() downloader.Elem {
method Err (line 323) | func (i *iter) Err() error {
method SetFinished (line 327) | func (i *iter) SetFinished(finished map[int]struct{}) {
method Finished (line 334) | func (i *iter) Finished() map[int]struct{} {
method Fingerprint (line 341) | func (i *iter) Fingerprint() string {
method Finish (line 345) | func (i *iter) Finish(id int) {
method Total (line 352) | func (i *iter) Total() int {
method SkippedDeleted (line 363) | func (i *iter) SkippedDeleted() int64 {
method DeletedIDs (line 367) | func (i *iter) DeletedIDs() []string {
function newIter (line 73) | func newIter(pool dcpool.Pool, manager *peers.Manager, dialog [][]*tmess...
function flatDialogs (line 379) | func flatDialogs(dialogs [][]*tmessage.Dialog) []*tmessage.Dialog {
function sortDialogs (line 390) | func sortDialogs(dialogs []*tmessage.Dialog, desc bool) {
function fingerprint (line 416) | func fingerprint(dialogs []*tmessage.Dialog) string {
FILE: app/dl/iter_test.go
function TestIterDeletedMessageHandling (line 19) | func TestIterDeletedMessageHandling(t *testing.T) {
function TestIterLogicalPositionIncrement (line 98) | func TestIterLogicalPositionIncrement(t *testing.T) {
function TestIterNoFatalErrorOnDeletedMessage (line 123) | func TestIterNoFatalErrorOnDeletedMessage(t *testing.T) {
function TestIterReturnValues (line 146) | func TestIterReturnValues(t *testing.T) {
function createDeletedMessageError (line 170) | func createDeletedMessageError(peerID int64, msgID int) error {
type deletedMessageError (line 180) | type deletedMessageError struct
method Error (line 185) | func (e *deletedMessageError) Error() string {
function TestDeletedMessageErrorFormat (line 191) | func TestDeletedMessageErrorFormat(t *testing.T) {
function BenchmarkDeletedMessageDetection (line 202) | func BenchmarkDeletedMessageDetection(b *testing.B) {
function TestIterContextCancellation (line 214) | func TestIterContextCancellation(t *testing.T) {
FILE: app/dl/progress.go
type progress (line 23) | type progress struct
method OnAdd (line 40) | func (p *progress) OnAdd(elem downloader.Elem) {
method OnDownload (line 45) | func (p *progress) OnDownload(elem downloader.Elem, state downloader.P...
method OnDone (line 56) | func (p *progress) OnDone(elem downloader.Elem, err error) {
method donePost (line 86) | func (p *progress) donePost(elem *iterElem) error {
method fail (line 116) | func (p *progress) fail(t *pw.Tracker, elem downloader.Elem, err error) {
method processMessage (line 121) | func (p *progress) processMessage(elem downloader.Elem) string {
method elemString (line 125) | func (p *progress) elemString(elem downloader.Elem) string {
function newProgress (line 31) | func newProgress(p pw.Writer, it *iter, opts Options) *progress {
FILE: app/dl/serve.go
type media (line 32) | type media struct
function serve (line 40) | func serve(ctx context.Context,
function handler (line 137) | func handler(h func(w http.ResponseWriter, r *http.Request) error) http....
function convItem (line 145) | func convItem(msg *tg.Message) (*media, error) {
FILE: app/extension/extension.go
function List (line 28) | func List(ctx context.Context, em *extensions.Manager) error {
function Install (line 49) | func Install(ctx context.Context, em *extensions.Manager, targets []stri...
function Upgrade (line 68) | func Upgrade(ctx context.Context, em *extensions.Manager, targets []stri...
function Remove (line 119) | func Remove(ctx context.Context, em *extensions.Manager, targets []strin...
function normalizeExtName (line 152) | func normalizeExtName(n string) string {
FILE: app/forward/elem.go
type iterElem (line 10) | type iterElem struct
method Mode (line 19) | func (i *iterElem) Mode() forwarder.Mode {
method From (line 26) | func (i *iterElem) From() peers.Peer { return i.from }
method Msg (line 28) | func (i *iterElem) Msg() *tg.Message { return i.msg }
method To (line 30) | func (i *iterElem) To() peers.Peer { return i.to }
method Thread (line 32) | func (i *iterElem) Thread() int { return i.thread }
method AsSilent (line 34) | func (i *iterElem) AsSilent() bool { return i.opts.silent }
method AsDryRun (line 36) | func (i *iterElem) AsDryRun() bool { return i.opts.dryRun }
method AsGrouped (line 38) | func (i *iterElem) AsGrouped() bool { return i.opts.grouped }
FILE: app/forward/forward.go
type Options (line 31) | type Options struct
function Run (line 42) | func Run(ctx context.Context, c *telegram.Client, kvd storage.Storage, o...
function collectDialogs (line 109) | func collectDialogs(ctx context.Context, input []string, desc bool) ([]*...
function resolveDest (line 146) | func resolveDest(ctx context.Context, manager *peers.Manager, input stri...
function resolveEdit (line 173) | func resolveEdit(input string) (*vm.Program, error) {
function totalMessages (line 193) | func totalMessages(dialogs []*tmessage.Dialog) int {
FILE: app/forward/iter.go
type iterOptions (line 23) | type iterOptions struct
type iter (line 36) | type iter struct
method Next (line 85) | func (i *iter) Next(ctx context.Context) bool {
method resolvePeer (line 202) | func (i *iter) resolvePeer(ctx context.Context, peer string) (peers.Pe...
method Value (line 210) | func (i *iter) Value() forwarder.Elem {
method Err (line 214) | func (i *iter) Err() error {
type env (line 44) | type env struct
function exprEnv (line 53) | func exprEnv(from peers.Peer, msg *tg.Message) env {
type dest (line 69) | type dest struct
function newIter (line 74) | func newIter(opts iterOptions) *iter {
FILE: app/forward/progress.go
type progress (line 16) | type progress struct
method OnAdd (line 36) | func (p *progress) OnAdd(elem forwarder.Elem) {
method OnClone (line 41) | func (p *progress) OnClone(elem forwarder.Elem, state forwarder.Progre...
method OnDone (line 54) | func (p *progress) OnDone(elem forwarder.Elem, err error) {
method tuple (line 72) | func (p *progress) tuple(elem forwarder.Elem) tuple {
method processMessage (line 80) | func (p *progress) processMessage(elem forwarder.Elem, clone bool) str...
method metaString (line 91) | func (p *progress) metaString(elem forwarder.Elem) string {
type tuple (line 22) | type tuple struct
function newProgress (line 28) | func newProgress(p pw.Writer) *progress {
FILE: app/internal/tctx/tctx.go
type kvKey (line 10) | type kvKey struct
function KV (line 12) | func KV(ctx context.Context) storage.Storage {
function WithKV (line 16) | func WithKV(ctx context.Context, kv storage.Storage) context.Context {
type poolKey (line 20) | type poolKey struct
function Pool (line 22) | func Pool(ctx context.Context) dcpool.Pool {
function WithPool (line 26) | func WithPool(ctx context.Context, pool dcpool.Pool) context.Context {
FILE: app/login/code.go
function Code (line 20) | func Code(ctx context.Context) error {
type noSignUp (line 63) | type noSignUp struct
method SignUp (line 65) | func (c noSignUp) SignUp(_ context.Context) (auth.UserInfo, error) {
method AcceptTermsOfService (line 69) | func (c noSignUp) AcceptTermsOfService(_ context.Context, tos tg.HelpT...
type termAuth (line 74) | type termAuth struct
method Phone (line 78) | func (a termAuth) Phone(_ context.Context) (string, error) {
method Password (line 93) | func (a termAuth) Password(_ context.Context) (string, error) {
method Code (line 106) | func (a termAuth) Code(_ context.Context, _ *tg.AuthSentCode) (string,...
FILE: app/login/desktop.go
constant tdata (line 27) | tdata = "tdata"
function Desktop (line 29) | func Desktop(ctx context.Context, opts Options) error {
function findDesktop (line 105) | func findDesktop(desktop string) (string, error) {
function detectAppData (line 125) | func detectAppData() string {
function appendTData (line 135) | func appendTData(path string) string {
function forceLogout (line 144) | func forceLogout(idx uint32, desktop string) error {
FILE: app/login/login.go
type Type (line 13) | type Type
type Options (line 15) | type Options struct
function Run (line 21) | func Run(ctx context.Context, opts Options) error {
FILE: app/login/login_enum.go
constant TypeDesktop (line 16) | TypeDesktop Type = iota
constant TypeCode (line 18) | TypeCode
constant TypeQr (line 20) | TypeQr
constant _TypeName (line 25) | _TypeName = "desktopcodeqr"
function TypeNames (line 34) | func TypeNames() []string {
function TypeValues (line 41) | func TypeValues() []Type {
method String (line 56) | func (x Type) String() string {
method IsValid (line 65) | func (x Type) IsValid() bool {
function ParseType (line 80) | func ParseType(name string) (Type, error) {
method Set (line 92) | func (x *Type) Set(val string) error {
method Get (line 99) | func (x *Type) Get() interface{} {
method Type (line 104) | func (x *Type) Type() string {
FILE: app/login/qr.go
function QR (line 24) | func QR(ctx context.Context) error {
FILE: app/migrate/backup.go
function Backup (line 16) | func Backup(ctx context.Context, dst string) (rerr error) {
FILE: app/migrate/migrate.go
function Migrate (line 13) | func Migrate(ctx context.Context, to map[string]string) error {
FILE: app/migrate/recover.go
function Recover (line 17) | func Recover(ctx context.Context, file string) (rerr error) {
FILE: app/up/elem.go
type iterElem (line 14) | type iterElem struct
method File (line 25) | func (e *iterElem) File() uploader.File {
method Thumb (line 29) | func (e *iterElem) Thumb() (uploader.File, bool) {
method Caption (line 36) | func (e *iterElem) Caption() (string, []tg.MessageEntityClass) {
method To (line 40) | func (e *iterElem) To() tg.InputPeerClass {
method Thread (line 44) | func (e *iterElem) Thread() int {
method AsPhoto (line 48) | func (e *iterElem) AsPhoto() bool {
type uploaderFile (line 52) | type uploaderFile struct
method Name (line 57) | func (u *uploaderFile) Name() string {
method Size (line 61) | func (u *uploaderFile) Size() int64 {
FILE: app/up/iter.go
type File (line 23) | type File struct
type dest (line 28) | type dest struct
type iter (line 33) | type iter struct
method Next (line 67) | func (i *iter) Next(ctx context.Context) bool {
method next (line 97) | func (i *iter) next(ctx context.Context, cur *File) (*iterElem, error) {
method resolveFile (line 132) | func (i *iter) resolveFile(path string) (*uploaderFile, error) {
method resolveDest (line 149) | func (i *iter) resolveDest(ctx context.Context, env Env) (peers.Peer, ...
method resolvePeer (line 196) | func (i *iter) resolvePeer(ctx context.Context, peer string) (peers.Pe...
method resolveCaption (line 204) | func (i *iter) resolveCaption(env Env) (*entity.Builder, error) {
method resolveThumb (line 229) | func (i *iter) resolveThumb(path string) (*uploaderFile, error) {
method Value (line 251) | func (i *iter) Value() uploader.Elem {
method Err (line 255) | func (i *iter) Err() error {
function newIter (line 49) | func newIter(files []*File, to, caption *vm.Program, chat string, topic ...
FILE: app/up/progress.go
type progress (line 17) | type progress struct
method OnAdd (line 34) | func (p *progress) OnAdd(elem uploader.Elem) {
method OnUpload (line 39) | func (p *progress) OnUpload(elem uploader.Elem, state uploader.Progres...
method OnDone (line 50) | func (p *progress) OnDone(elem uploader.Elem, err error) {
method closeFile (line 76) | func (p *progress) closeFile(e *iterElem) error {
method fail (line 90) | func (p *progress) fail(t *pw.Tracker, elem uploader.Elem, err error) {
method tuple (line 95) | func (p *progress) tuple(elem uploader.Elem) tuple {
method processMessage (line 99) | func (p *progress) processMessage(elem uploader.Elem) string {
method elemString (line 103) | func (p *progress) elemString(elem uploader.Elem) string {
type tuple (line 22) | type tuple struct
function newProgress (line 27) | func newProgress(p pw.Writer) *progress {
FILE: app/up/up.go
type Options (line 34) | type Options struct
type Env (line 46) | type Env struct
function Run (line 54) | func Run(ctx context.Context, c *telegram.Client, kvd storage.Storage, o...
function resolveDest (line 110) | func resolveDest(ctx context.Context, manager *peers.Manager, input stri...
function resolveCaption (line 130) | func resolveCaption(ctx context.Context, input string) (*vm.Program, err...
function exprEnv (line 150) | func exprEnv(ctx context.Context, file *File) Env {
FILE: app/up/walk.go
function walk (line 13) | func walk(paths, includes, excludes []string) ([]*File, error) {
FILE: cmd/chat.go
function NewChat (line 22) | func NewChat() *cobra.Command {
function NewChatList (line 34) | func NewChatList() *cobra.Command {
function NewChatExport (line 53) | func NewChatExport() *cobra.Command {
function NewChatUsers (line 134) | func NewChatUsers() *cobra.Command {
FILE: cmd/dl.go
function NewDownload (line 17) | func NewDownload() *cobra.Command {
FILE: cmd/extension.go
function NewExtension (line 22) | func NewExtension(em *extensions.Manager) *cobra.Command {
function NewExtensionList (line 42) | func NewExtensionList(em *extensions.Manager) *cobra.Command {
function NewExtensionInstall (line 56) | func NewExtensionInstall(em *extensions.Manager) *cobra.Command {
function NewExtensionUpgrade (line 73) | func NewExtensionUpgrade(em *extensions.Manager) *cobra.Command {
function NewExtensionRemove (line 85) | func NewExtensionRemove(em *extensions.Manager) *cobra.Command {
function NewExtensionCmd (line 98) | func NewExtensionCmd(em *extensions.Manager, ext extensions.Extension, s...
FILE: cmd/forward.go
function NewForward (line 17) | func NewForward() *cobra.Command {
FILE: cmd/gen.go
function NewGen (line 17) | func NewGen() *cobra.Command {
function NewGenDoc (line 29) | func NewGenDoc() *cobra.Command {
FILE: cmd/login.go
function NewLogin (line 14) | func NewLogin() *cobra.Command {
FILE: cmd/migrate.go
function NewBackup (line 14) | func NewBackup() *cobra.Command {
function NewRecover (line 35) | func NewRecover() *cobra.Command {
function NewMigrate (line 58) | func NewMigrate() *cobra.Command {
FILE: cmd/root.go
function New (line 62) | func New() *cobra.Command {
type completeFunc (line 198) | type completeFunc
function completeExtFiles (line 200) | func completeExtFiles(ext ...string) completeFunc {
function tOptions (line 215) | func tOptions(ctx context.Context) (tclient.Options, error) {
function tRun (line 232) | func tRun(ctx context.Context, f func(ctx context.Context, c *telegram.C...
function migrateLegacyToBolt (line 248) | func migrateLegacyToBolt() (rerr error) {
FILE: cmd/up.go
function NewUpload (line 15) | func NewUpload() *cobra.Command {
FILE: cmd/version.go
function NewVersion (line 18) | func NewVersion() *cobra.Command {
FILE: core/dcpool/dcpool.go
function EnableTestMode (line 19) | func EnableTestMode() {
type Pool (line 23) | type Pool interface
type pool (line 30) | type pool struct
method current (line 53) | func (p *pool) current() int {
method Client (line 57) | func (p *pool) Client(ctx context.Context, dc int) *tg.Client {
method invoker (line 64) | func (p *pool) invoker(ctx context.Context, dc int) tg.Invoker {
method Default (line 97) | func (p *pool) Default(ctx context.Context) *tg.Client {
method Close (line 101) | func (p *pool) Close() (err error) {
method Takeout (line 113) | func (p *pool) Takeout(ctx context.Context, dc int) *tg.Client {
function NewPool (line 41) | func NewPool(c *telegram.Client, size int64, middlewares ...telegram.Mid...
FILE: core/dcpool/middlewares.go
function chainMiddlewares (line 8) | func chainMiddlewares(invoker tg.Invoker, chain ...telegram.Middleware) ...
FILE: core/downloader/downloader.go
constant MaxPartSize (line 17) | MaxPartSize = 1024 * 1024
type Downloader (line 19) | type Downloader struct
method Download (line 36) | func (d *Downloader) Download(ctx context.Context, limit int) error {
method download (line 73) | func (d *Downloader) download(ctx context.Context, elem Elem) error {
type Options (line 23) | type Options struct
function New (line 30) | func New(opts Options) *Downloader {
FILE: core/downloader/iter.go
type Iter (line 10) | type Iter interface
type Elem (line 16) | type Elem interface
type File (line 23) | type File interface
FILE: core/downloader/progress.go
type Progress (line 9) | type Progress interface
type ProgressState (line 16) | type ProgressState struct
type writeAt (line 24) | type writeAt struct
method WriteAt (line 41) | func (w *writeAt) WriteAt(p []byte, off int64) (int, error) {
function newWriteAt (line 32) | func newWriteAt(elem Elem, progress Progress, partSize int) *writeAt {
FILE: core/forwarder/clone.go
type cloneOptions (line 21) | type cloneOptions struct
type progressAdd (line 27) | type progressAdd interface
method cloneMedia (line 31) | func (f *Forwarder) cloneMedia(ctx context.Context, opts cloneOptions, d...
type writeAt (line 85) | type writeAt struct
method WriteAt (line 90) | func (w writeAt) WriteAt(p []byte, off int64) (int, error) {
type uploaded (line 101) | type uploaded struct
method Chunk (line 106) | func (u uploaded) Chunk(_ context.Context, state uploader.ProgressStat...
FILE: core/forwarder/forwarder.go
type Mode (line 25) | type Mode
type Options (line 27) | type Options struct
type Forwarder (line 34) | type Forwarder struct
method Forward (line 53) | func (f *Forwarder) Forward(ctx context.Context) error {
method forwardMessage (line 86) | func (f *Forwarder) forwardMessage(ctx context.Context, elem Elem, gro...
method tuple (line 378) | func (f *Forwarder) tuple(peer peers.Peer, msg *tg.Message) tuple {
method forwardClient (line 409) | func (f *Forwarder) forwardClient(ctx context.Context, elem Elem) *tg....
type tuple (line 40) | type tuple struct
function New (line 45) | func New(opts Options) *Forwarder {
type nopInvoker (line 385) | type nopInvoker struct
method Invoke (line 387) | func (n nopInvoker) Invoke(_ context.Context, _ bin.Encoder, _ bin.Dec...
type nopProgress (line 391) | type nopProgress struct
method add (line 393) | func (nopProgress) add(_ int64) {}
type wrapProgress (line 395) | type wrapProgress struct
method add (line 402) | func (w *wrapProgress) add(n int64) {
function protectedDialog (line 417) | func protectedDialog(peer peers.Peer) bool {
function protectedMessage (line 428) | func protectedMessage(msg *tg.Message) bool {
function photoOrDocument (line 432) | func photoOrDocument(media tg.MessageMediaClass) bool {
function mediaSizeSum (line 441) | func mediaSizeSum(msg *tg.Message, grouped ...*tg.Message) (int64, error) {
function getReplyTo (line 463) | func getReplyTo(thread int) tg.InputReplyToClass {
FILE: core/forwarder/forwarder_enum.go
constant ModeDirect (line 16) | ModeDirect Mode = iota
constant ModeClone (line 18) | ModeClone
constant _ModeName (line 23) | _ModeName = "directclone"
function ModeNames (line 31) | func ModeNames() []string {
function ModeValues (line 38) | func ModeValues() []Mode {
method String (line 51) | func (x Mode) String() string {
method IsValid (line 60) | func (x Mode) IsValid() bool {
function ParseMode (line 73) | func ParseMode(name string) (Mode, error) {
method Set (line 85) | func (x *Mode) Set(val string) error {
method Get (line 92) | func (x *Mode) Get() interface{} {
method Type (line 97) | func (x *Mode) Type() string {
FILE: core/forwarder/iter.go
type Iter (line 10) | type Iter interface
type Elem (line 16) | type Elem interface
FILE: core/forwarder/progress.go
type ProgressClone (line 3) | type ProgressClone interface
type Progress (line 7) | type Progress interface
type ProgressState (line 13) | type ProgressState struct
FILE: core/logctx/logctx.go
type ctxKey (line 9) | type ctxKey struct
function From (line 11) | func From(ctx context.Context) *zap.Logger {
function With (line 18) | func With(ctx context.Context, logger *zap.Logger) context.Context {
function Named (line 22) | func Named(ctx context.Context, name string) context.Context {
FILE: core/middlewares/recovery/recovery.go
type recovery (line 18) | type recovery struct
method Handle (line 30) | func (r *recovery) Handle(next tg.Invoker) telegram.InvokeFunc {
method shouldRecover (line 50) | func (r *recovery) shouldRecover(ctx context.Context, err error) bool {
function New (line 23) | func New(ctx context.Context, backoff backoff.BackOff) telegram.Middlewa...
FILE: core/middlewares/retry/retry.go
type retry (line 26) | type retry struct
method Handle (line 31) | func (r retry) Handle(next tg.Invoker) telegram.InvokeFunc {
function New (line 53) | func New(max int, errors ...string) telegram.Middleware {
FILE: core/middlewares/takeout/middleware.go
type takeout (line 12) | type takeout struct
method Handle (line 24) | func (t takeout) Handle(next tg.Invoker) telegram.InvokeFunc {
type nopDecoder (line 16) | type nopDecoder struct
method Decode (line 20) | func (n nopDecoder) Decode(_ *bin.Buffer) error {
function Middleware (line 33) | func Middleware(id int64) telegram.Middleware {
FILE: core/middlewares/takeout/takeout.go
function Takeout (line 9) | func Takeout(ctx context.Context, invoker tg.Invoker) (int64, error) {
function UnTakeout (line 29) | func UnTakeout(ctx context.Context, invoker tg.Invoker) error {
FILE: core/storage/keygen/keygen.go
function New (line 17) | func New(indexes ...string) string {
FILE: core/storage/peers.go
type Peers (line 14) | type Peers struct
method Save (line 22) | func (p *Peers) Save(ctx context.Context, key peers.Key, value peers.V...
method Find (line 31) | func (p *Peers) Find(ctx context.Context, key peers.Key) (peers.Value,...
method SavePhone (line 48) | func (p *Peers) SavePhone(ctx context.Context, phone string, _key peer...
method FindPhone (line 57) | func (p *Peers) FindPhone(ctx context.Context, phone string) (peers.Ke...
method GetContactsHash (line 79) | func (p *Peers) GetContactsHash(ctx context.Context) (int64, error) {
method SaveContactsHash (line 91) | func (p *Peers) SaveContactsHash(ctx context.Context, hash int64) error {
method key (line 95) | func (p *Peers) key(key peers.Key) string {
method phoneKey (line 99) | func (p *Peers) phoneKey(phone string) string {
method contactsKey (line 103) | func (p *Peers) contactsKey() string {
function NewPeers (line 18) | func NewPeers(kv Storage) peers.Storage {
FILE: core/storage/session.go
type Session (line 12) | type Session struct
method LoadSession (line 21) | func (s *Session) LoadSession(ctx context.Context) ([]byte, error) {
method StoreSession (line 36) | func (s *Session) StoreSession(ctx context.Context, data []byte) error {
method key (line 40) | func (s *Session) key() string {
function NewSession (line 17) | func NewSession(kv Storage, login bool) telegram.SessionStorage {
FILE: core/storage/state.go
type State (line 14) | type State struct
method Get (line 22) | func (s *State) Get(ctx context.Context, key string, v interface{}) er...
method Set (line 31) | func (s *State) Set(ctx context.Context, key string, v interface{}) er...
method GetState (line 40) | func (s *State) GetState(ctx context.Context, userID int64) (updates.S...
method SetState (line 53) | func (s *State) SetState(ctx context.Context, userID int64, state upda...
method SetPts (line 61) | func (s *State) SetPts(ctx context.Context, userID int64, pts int) err...
method SetQts (line 71) | func (s *State) SetQts(ctx context.Context, userID int64, qts int) err...
method SetDate (line 81) | func (s *State) SetDate(ctx context.Context, userID int64, date int) e...
method SetSeq (line 91) | func (s *State) SetSeq(ctx context.Context, userID int64, seq int) err...
method SetDateSeq (line 101) | func (s *State) SetDateSeq(ctx context.Context, userID int64, date, se...
method GetChannelPts (line 112) | func (s *State) GetChannelPts(ctx context.Context, userID, channelID i...
method SetChannelPts (line 130) | func (s *State) SetChannelPts(ctx context.Context, userID, channelID i...
method ForEachChannels (line 140) | func (s *State) ForEachChannels(ctx context.Context, userID int64, f f...
method stateKey (line 156) | func (s *State) stateKey(userID int64) string {
method channelKey (line 160) | func (s *State) channelKey(userID int64) string {
function NewState (line 18) | func NewState(kv Storage) updates.StateStorage {
FILE: core/storage/storage.go
type Storage (line 9) | type Storage interface
FILE: core/tclient/tclient.go
type Options (line 32) | type Options struct
function New (line 45) | func New(ctx context.Context, o Options) (*telegram.Client, error) {
function NewDefaultMiddlewares (line 90) | func NewDefaultMiddlewares(ctx context.Context, timeout time.Duration) [...
function newBackoff (line 98) | func newBackoff(timeout time.Duration) backoff.BackOff {
function RunWithAuth (line 107) | func RunWithAuth(ctx context.Context, client *telegram.Client, f func(ct...
FILE: core/tmedia/convert.go
function ConvInputMedia (line 7) | func ConvInputMedia(media tg.MessageMediaClass) (tg.InputMediaClass, boo...
function ConvInputMediaPhoto (line 38) | func ConvInputMediaPhoto(v *tg.MessageMediaPhoto) (*tg.InputMediaPhoto, ...
function ConvInputMediaGeo (line 58) | func ConvInputMediaGeo(v *tg.MessageMediaGeo) (*tg.InputMediaGeoPoint, b...
function ConvInputMediaContact (line 75) | func ConvInputMediaContact(v *tg.MessageMediaContact) (*tg.InputMediaCon...
function ConvInputMediaDocument (line 82) | func ConvInputMediaDocument(v *tg.MessageMediaDocument) (*tg.InputMediaD...
function ConvInputMediaVenue (line 102) | func ConvInputMediaVenue(v *tg.MessageMediaVenue) (*tg.InputMediaVenue, ...
function ConvInputMediaGame (line 118) | func ConvInputMediaGame(v *tg.MessageMediaGame) (*tg.InputMediaGame, boo...
function ConvInputMediaInvoice (line 127) | func ConvInputMediaInvoice(v *tg.MessageMediaInvoice) (*tg.InputMediaInv...
function ConvInputMediaGeoLive (line 133) | func ConvInputMediaGeoLive(v *tg.MessageMediaGeoLive) (*tg.InputMediaGeo...
function ConvInputMediaPoll (line 139) | func ConvInputMediaPoll(v *tg.MessageMediaPoll) (*tg.InputMediaPoll, boo...
function ConvInputMediaDice (line 145) | func ConvInputMediaDice(v *tg.MessageMediaDice) (*tg.InputMediaDice, boo...
function ConvInputMediaStory (line 151) | func ConvInputMediaStory(v *tg.MessageMediaStory) (*tg.InputMediaStory, ...
FILE: core/tmedia/document.go
function GetDocumentInfo (line 10) | func GetDocumentInfo(doc *tg.MessageMediaDocument) (*Media, bool) {
function GetDocumentName (line 29) | func GetDocumentName(doc *tg.Document) string {
FILE: core/tmedia/media.go
type Media (line 7) | type Media struct
function ExtractMedia (line 15) | func ExtractMedia(m tg.MessageMediaClass) (*Media, bool) {
function GetMedia (line 27) | func GetMedia(msg tg.MessageClass) (*Media, bool) {
function GetExtendedMedia (line 41) | func GetExtendedMedia(mm tg.MessageExtendedMediaClass) (*Media, bool) {
function GetDocumentThumb (line 49) | func GetDocumentThumb(doc *tg.Document) (*Media, bool) {
FILE: core/tmedia/photo.go
function GetPhotoInfo (line 9) | func GetPhotoInfo(photo *tg.MessageMediaPhoto) (*Media, bool) {
function GetPhotoSize (line 34) | func GetPhotoSize(sizes []tg.PhotoSizeClass) (string, int, bool) {
FILE: core/uploader/iter.go
type Iter (line 10) | type Iter interface
type File (line 16) | type File interface
type Elem (line 22) | type Elem interface
FILE: core/uploader/progress.go
type Progress (line 9) | type Progress interface
type ProgressState (line 16) | type ProgressState struct
type wrapProcess (line 21) | type wrapProcess struct
method Chunk (line 26) | func (p *wrapProcess) Chunk(_ context.Context, state uploader.Progress...
FILE: core/uploader/uploader.go
constant MaxPartSize (line 23) | MaxPartSize = 512 * 1024
type Uploader (line 25) | type Uploader struct
method Upload (line 40) | func (u *Uploader) Upload(ctx context.Context, limit int) error {
method upload (line 71) | func (u *Uploader) upload(ctx context.Context, elem Elem) error {
type Options (line 29) | type Options struct
function New (line 36) | func New(o Options) *Uploader {
FILE: core/util/fsutil/fsutil.go
function GetNameWithoutExt (line 9) | func GetNameWithoutExt(path string) string {
function PathExists (line 13) | func PathExists(path string) bool {
function AddPrefixDot (line 19) | func AddPrefixDot(ext string) string {
FILE: core/util/logutil/logutil.go
function New (line 9) | func New(level zapcore.LevelEnabler, path string) *zap.Logger {
FILE: core/util/mediautil/mediautil.go
function split (line 11) | func split(mime string) (primary string, sub string, ok bool) {
function IsVideo (line 21) | func IsVideo(mime string) bool {
function IsAudio (line 27) | func IsAudio(mime string) bool {
function IsImage (line 33) | func IsImage(mime string) bool {
function GetMP4Info (line 40) | func GetMP4Info(r io.ReadSeeker) (int, int, int, error) {
FILE: core/util/netutil/netutil.go
function init (line 11) | func init() {
function NewProxy (line 17) | func NewProxy(proxyUrl string) (proxy.ContextDialer, error) {
FILE: core/util/tutil/tutil.go
function ParseMessageLink (line 20) | func ParseMessageLink(ctx context.Context, manager *peers.Manager, s str...
function GetInputPeer (line 94) | func GetInputPeer(ctx context.Context, manager *peers.Manager, from stri...
function GetPeerID (line 120) | func GetPeerID(peer tg.PeerClass) int64 {
function GetInputPeerID (line 132) | func GetInputPeerID(peer tg.InputPeerClass) int64 {
function GetBlockedDialogs (line 145) | func GetBlockedDialogs(ctx context.Context, client *tg.Client) (map[int6...
function FileExists (line 158) | func FileExists(msg tg.MessageClass) bool {
function GetSingleMessage (line 177) | func GetSingleMessage(ctx context.Context, c *tg.Client, peer tg.InputPe...
type Messages (line 199) | type Messages
method Len (line 201) | func (m Messages) Len() int {
method Less (line 205) | func (m Messages) Less(i, j int) bool {
method Swap (line 209) | func (m Messages) Swap(i, j int) {
function GetGroupedMessages (line 213) | func GetGroupedMessages(ctx context.Context, c *tg.Client, peer tg.Input...
function BestThreads (line 267) | func BestThreads(size int64, max int) int {
FILE: extension/extension.go
constant EnvKey (line 21) | EnvKey = "TDL_EXTENSION"
type Env (line 23) | type Env struct
type Options (line 36) | type Options struct
type Extension (line 47) | type Extension struct
method Name (line 62) | func (e *Extension) Name() string {
method Client (line 66) | func (e *Extension) Client() *telegram.Client {
method Log (line 70) | func (e *Extension) Log() *zap.Logger {
method Config (line 74) | func (e *Extension) Config() *Config {
type Config (line 54) | type Config struct
type Handler (line 78) | type Handler
function New (line 80) | func New(o Options) func(h Handler) {
function buildExtension (line 102) | func buildExtension(ctx context.Context, o Options) (*Extension, *telegr...
function buildClient (line 152) | func buildClient(ctx context.Context, env *Env, o Options) (*telegram.Cl...
function assert (line 170) | func assert(err error) {
FILE: main.go
function main (line 16) | func main() {
FILE: pkg/clock/clock.go
constant defaultHost (line 11) | defaultHost = "pool.ntp.org"
type networkClock (line 13) | type networkClock struct
method Now (line 17) | func (n *networkClock) Now() time.Time {
method Timer (line 21) | func (n *networkClock) Timer(d time.Duration) clock.Timer {
method Ticker (line 25) | func (n *networkClock) Ticker(d time.Duration) clock.Ticker {
function New (line 30) | func New(ntpHost ...string) (clock.Clock, error) {
FILE: pkg/consts/consts.go
function init (line 8) | func init() {
FILE: pkg/consts/flag.go
constant FlagStorage (line 4) | FlagStorage = "storage"
constant FlagProxy (line 5) | FlagProxy = "proxy"
constant FlagNamespace (line 6) | FlagNamespace = "ns"
constant FlagDebug (line 7) | FlagDebug = "debug"
constant FlagPartSize (line 8) | FlagPartSize = "size"
constant FlagThreads (line 9) | FlagThreads = "threads"
constant FlagLimit (line 10) | FlagLimit = "limit"
constant FlagPoolSize (line 11) | FlagPoolSize = "pool"
constant FlagDelay (line 12) | FlagDelay = "delay"
constant FlagNTP (line 13) | FlagNTP = "ntp"
constant FlagReconnectTimeout (line 14) | FlagReconnectTimeout = "reconnect-timeout"
constant FlagDlTemplate (line 15) | FlagDlTemplate = "template"
FILE: pkg/extensions/extensions.go
constant Prefix (line 11) | Prefix = "tdl-"
type ExtensionType (line 14) | type ExtensionType
type Extension (line 16) | type Extension interface
type baseExtension (line 27) | type baseExtension struct
method Name (line 31) | func (e baseExtension) Name() string {
method Path (line 37) | func (e baseExtension) Path() string {
type manifest (line 41) | type manifest struct
FILE: pkg/extensions/extensions_enum.go
constant ExtensionTypeGithub (line 16) | ExtensionTypeGithub ExtensionType = "github"
constant ExtensionTypeLocal (line 18) | ExtensionTypeLocal ExtensionType = "local"
function ExtensionTypeNames (line 29) | func ExtensionTypeNames() []string {
function ExtensionTypeValues (line 36) | func ExtensionTypeValues() []ExtensionType {
method String (line 44) | func (x ExtensionType) String() string {
method IsValid (line 50) | func (x ExtensionType) IsValid() bool {
function ParseExtensionType (line 61) | func ParseExtensionType(name string) (ExtensionType, error) {
method Set (line 73) | func (x *ExtensionType) Set(val string) error {
method Get (line 80) | func (x *ExtensionType) Get() interface{} {
method Type (line 85) | func (x *ExtensionType) Type() string {
FILE: pkg/extensions/extensions_test.go
function TestBaseExtension (line 9) | func TestBaseExtension(t *testing.T) {
FILE: pkg/extensions/github.go
constant githubHost (line 16) | githubHost = "github.com"
constant manifestName (line 17) | manifestName = "manifest.json"
type githubExtension (line 20) | type githubExtension struct
method Type (line 31) | func (e *githubExtension) Type() ExtensionType {
method URL (line 35) | func (e *githubExtension) URL() string {
method Owner (line 43) | func (e *githubExtension) Owner() string {
method CurrentVersion (line 51) | func (e *githubExtension) CurrentVersion() string {
method LatestVersion (line 59) | func (e *githubExtension) LatestVersion(ctx context.Context) string {
method loadManifest (line 84) | func (e *githubExtension) loadManifest() (*manifest, error) {
method UpdateAvailable (line 113) | func (e *githubExtension) UpdateAvailable(ctx context.Context) bool {
FILE: pkg/extensions/local.go
type localExtension (line 8) | type localExtension struct
method Type (line 12) | func (l *localExtension) Type() ExtensionType {
method URL (line 16) | func (l *localExtension) URL() string {
method Owner (line 20) | func (l *localExtension) Owner() string {
method CurrentVersion (line 24) | func (l *localExtension) CurrentVersion() string {
method LatestVersion (line 28) | func (l *localExtension) LatestVersion(_ context.Context) string {
method UpdateAvailable (line 32) | func (l *localExtension) UpdateAvailable(_ context.Context) bool {
FILE: pkg/extensions/local_test.go
function TestLocalExtension (line 10) | func TestLocalExtension(t *testing.T) {
FILE: pkg/extensions/manager.go
type Manager (line 29) | type Manager struct
method SetDryRun (line 54) | func (m *Manager) SetDryRun(v bool) {
method DryRun (line 58) | func (m *Manager) DryRun() bool {
method SetClient (line 62) | func (m *Manager) SetClient(client *http.Client) {
method Dispatch (line 67) | func (m *Manager) Dispatch(ext Extension, args []string, env *extensio...
method List (line 97) | func (m *Manager) List(ctx context.Context, includeLatestVersion bool)...
method Upgrade (line 133) | func (m *Manager) Upgrade(ctx context.Context, ext Extension) error {
method Install (line 164) | func (m *Manager) Install(ctx context.Context, target string, force bo...
method installLocal (line 179) | func (m *Manager) installLocal(path string, force bool) error {
method installGitHub (line 212) | func (m *Manager) installGitHub(ctx context.Context, owner, repo strin...
method maybeExist (line 273) | func (m *Manager) maybeExist(binPath string, force bool) error {
method Remove (line 296) | func (m *Manager) Remove(ext Extension) error {
method populateLatestVersions (line 310) | func (m *Manager) populateLatestVersions(ctx context.Context, exts []E...
method downloadGitHubAsset (line 322) | func (m *Manager) downloadGitHubAsset(ctx context.Context, owner, repo...
function NewManager (line 37) | func NewManager(dir string) *Manager {
function newGhClient (line 46) | func newGhClient(c *http.Client) *github.Client {
function copyRegularFile (line 341) | func copyRegularFile(src, dst string) (rerr error) {
function platformBinaryName (line 368) | func platformBinaryName() (string, string) {
function extractGOARM (line 385) | func extractGOARM() string {
FILE: pkg/filterMap/filterMap.go
function New (line 3) | func New(data []string, keyFn func(key string) string) map[string]struct...
FILE: pkg/key/key.go
function App (line 7) | func App() string {
function Resume (line 11) | func Resume(fingerprint string) string {
FILE: pkg/kv/bolt.go
function init (line 17) | func init() {
type bolt (line 21) | type bolt struct
method Name (line 52) | func (b *bolt) Name() string {
method MigrateTo (line 56) | func (b *bolt) MigrateTo() (Meta, error) {
method MigrateFrom (line 81) | func (b *bolt) MigrateFrom(meta Meta) error {
method Namespaces (line 107) | func (b *bolt) Namespaces() ([]string, error) {
method walk (line 119) | func (b *bolt) walk(fn func(path string) error) error {
method Open (line 132) | func (b *bolt) Open(ns string) (storage.Storage, error) {
method open (line 136) | func (b *bolt) open(ns string) (*legacyKV, error) {
method Close (line 163) | func (b *bolt) Close() error {
function newBolt (line 27) | func newBolt(opts map[string]any) (*bolt, error) {
FILE: pkg/kv/file.go
function init (line 17) | func init() {
type file (line 21) | type file struct
method Name (line 59) | func (f *file) Name() string {
method MigrateTo (line 63) | func (f *file) MigrateTo() (Meta, error) {
method MigrateFrom (line 71) | func (f *file) MigrateFrom(meta Meta) error {
method Namespaces (line 75) | func (f *file) Namespaces() ([]string, error) {
method Open (line 89) | func (f *file) Open(ns string) (storage.Storage, error) {
method Close (line 109) | func (f *file) Close() error {
method read (line 113) | func (f *file) read() (map[string]map[string][]byte, error) {
method write (line 130) | func (f *file) write(m map[string]map[string][]byte) error {
function newFile (line 26) | func newFile(opts map[string]any) (Storage, error) {
type fileKV (line 142) | type fileKV struct
method Get (line 147) | func (f *fileKV) Get(_ context.Context, key string) ([]byte, error) {
method Set (line 159) | func (f *fileKV) Set(_ context.Context, key string, value []byte) error {
method Delete (line 170) | func (f *fileKV) Delete(_ context.Context, key string) error {
FILE: pkg/kv/kv.go
type Driver (line 16) | type Driver
constant DriverTypeKey (line 18) | DriverTypeKey = "type"
type Meta (line 20) | type Meta
type Storage (line 22) | type Storage interface
function register (line 33) | func register(name Driver, fn func(map[string]any) (Storage, error)) {
function New (line 37) | func New(driver Driver, opts map[string]any) (Storage, error) {
function NewWithMap (line 45) | func NewWithMap(o map[string]string) (Storage, error) {
type ctxKey (line 63) | type ctxKey struct
function With (line 65) | func With(ctx context.Context, kv Storage) context.Context {
function From (line 69) | func From(ctx context.Context) Storage {
FILE: pkg/kv/kv_enum.go
constant DriverLegacy (line 16) | DriverLegacy Driver = "legacy"
constant DriverBolt (line 18) | DriverBolt Driver = "bolt"
constant DriverFile (line 20) | DriverFile Driver = "file"
function DriverNames (line 32) | func DriverNames() []string {
function DriverValues (line 39) | func DriverValues() []Driver {
method String (line 48) | func (x Driver) String() string {
method IsValid (line 54) | func (x Driver) IsValid() bool {
function ParseDriver (line 66) | func ParseDriver(name string) (Driver, error) {
method Set (line 78) | func (x *Driver) Set(val string) error {
method Get (line 85) | func (x *Driver) Get() interface{} {
method Type (line 90) | func (x *Driver) Type() string {
FILE: pkg/kv/kv_test.go
function forEachStorage (line 13) | func forEachStorage(t *testing.T, fn func(e Storage, t *testing.T)) {
function TestNew (line 31) | func TestNew(t *testing.T) {
function TestStorage_Open (line 70) | func TestStorage_Open(t *testing.T) {
function TestStorage_Namespaces (line 80) | func TestStorage_Namespaces(t *testing.T) {
function TestStorage_MigrateTo (line 96) | func TestStorage_MigrateTo(t *testing.T) {
function TestStorage_MigrateFrom (line 127) | func TestStorage_MigrateFrom(t *testing.T) {
FILE: pkg/kv/legacy.go
function init (line 22) | func init() {
function newLegacy (line 28) | func newLegacy(opts map[string]any) (*legacy, error) {
type legacy (line 50) | type legacy struct
method Name (line 54) | func (l *legacy) Name() string {
method MigrateTo (line 58) | func (l *legacy) MigrateTo() (Meta, error) {
method MigrateFrom (line 77) | func (l *legacy) MigrateFrom(meta Meta) error {
method Namespaces (line 94) | func (l *legacy) Namespaces() ([]string, error) {
method Open (line 107) | func (l *legacy) Open(ns string) (storage.Storage, error) {
method open (line 111) | func (l *legacy) open(ns string) (*legacyKV, error) {
method Close (line 125) | func (l *legacy) Close() error {
type legacyKV (line 129) | type legacyKV struct
method Get (line 134) | func (l *legacyKV) Get(_ context.Context, key string) ([]byte, error) {
method Set (line 150) | func (l *legacyKV) Set(_ context.Context, key string, value []byte) er...
method Delete (line 156) | func (l *legacyKV) Delete(_ context.Context, key string) error {
FILE: pkg/prog/prog.go
function New (line 13) | func New(formatter progress.UnitsFormatter) progress.Writer {
function Wait (line 44) | func Wait(ctx context.Context, pw progress.Writer) {
FILE: pkg/prog/tracker.go
function AppendTracker (line 13) | func AppendTracker(pw progress.Writer, formatter progress.UnitsFormatter...
function EnablePS (line 29) | func EnablePS(ctx context.Context, pw progress.Writer) {
FILE: pkg/ps/ps.go
function Humanize (line 17) | func Humanize(ctx context.Context) []string {
function init (line 33) | func init() {
function GetSelfCPU (line 41) | func GetSelfCPU(ctx context.Context) (float64, error) {
function GetSelfMem (line 51) | func GetSelfMem(ctx context.Context) (*process.MemoryInfoStat, error) {
function GetGoroutineNum (line 60) | func GetGoroutineNum() int {
FILE: pkg/tclient/app.go
constant AppBuiltin (line 4) | AppBuiltin = "builtin"
constant AppDesktop (line 5) | AppDesktop = "desktop"
type App (line 8) | type App struct
FILE: pkg/tclient/tclient.go
type Options (line 16) | type Options struct
function GetApp (line 24) | func GetApp(kv storage.Storage) (App, error) {
function New (line 37) | func New(ctx context.Context, o Options, login bool, middlewares ...tele...
FILE: pkg/tdesktop/tdesktop.go
function FileKey (line 11) | func FileKey(_ string) string
FILE: pkg/texpr/env.go
type EnvMessage (line 10) | type EnvMessage struct
type EnvMessageMedia (line 24) | type EnvMessageMedia struct
function ConvertEnvMessage (line 30) | func ConvertEnvMessage(msg *tg.Message) EnvMessage {
FILE: pkg/texpr/env_test.go
function TestMessageExpr (line 9) | func TestMessageExpr(t *testing.T) {
FILE: pkg/texpr/expr.go
function Run (line 15) | func Run(program *vm.Program, env any) (any, error) {
FILE: pkg/texpr/fields.go
type FieldsGetter (line 11) | type FieldsGetter struct
method Sprint (line 39) | func (f *FieldsGetter) Sprint(fields []*Field, colorable bool) string {
method Walk (line 64) | func (f *FieldsGetter) Walk(v any) ([]*Field, error) {
method walk (line 76) | func (f *FieldsGetter) walk(v reflect.Type, field *Field, fields *[]*F...
type Field (line 15) | type Field struct
type Options (line 21) | type Options struct
type Option (line 25) | type Option
function NewFieldsGetter (line 27) | func NewFieldsGetter(opts *Options) *FieldsGetter {
FILE: pkg/texpr/fields_test.go
function TestFieldsGetter (line 10) | func TestFieldsGetter(t *testing.T) {
FILE: pkg/tmessage/files.go
constant keyID (line 23) | keyID = "id"
constant typeMessage (line 24) | typeMessage = "message"
type fMessage (line 27) | type fMessage struct
function FromFile (line 38) | func FromFile(ctx context.Context, pool dcpool.Pool, kvd storage.Storage...
function parseFile (line 58) | func parseFile(ctx context.Context, client *tg.Client, kvd storage.Stora...
function collect (line 82) | func collect(ctx context.Context, r io.Reader, peer peers.Peer, onlyMedi...
function getChatInfo (line 120) | func getChatInfo(ctx context.Context, client *tg.Client, kvd storage.Sto...
FILE: pkg/tmessage/tmessage.go
type Dialog (line 7) | type Dialog struct
type ParseSource (line 12) | type ParseSource
function Parse (line 14) | func Parse(src ParseSource) ([]*Dialog, error) {
FILE: pkg/tmessage/urls.go
function FromURL (line 15) | func FromURL(ctx context.Context, pool dcpool.Pool, kvd storage.Storage,...
FILE: pkg/tpath/tpath.go
constant AppName (line 3) | AppName = "Telegram Desktop"
type desktop (line 5) | type desktop struct
method AppData (line 10) | func (desktop) AppData(homedir string) []string {
FILE: pkg/tpath/tpath_darwin.go
function desktopAppData (line 10) | func desktopAppData(homedir string) []string {
FILE: pkg/tpath/tpath_linux.go
function desktopAppData (line 12) | func desktopAppData(homedir string) []string {
FILE: pkg/tpath/tpath_other.go
function desktopAppData (line 5) | func desktopAppData(_ string) []string {
FILE: pkg/tpath/tpath_windows.go
function desktopAppData (line 11) | func desktopAppData(_ string) []string {
FILE: pkg/tplfunc/date.go
function Now (line 12) | func Now() Func {
function FormatDate (line 20) | func FormatDate() Func {
FILE: pkg/tplfunc/date_test.go
function TestNow (line 12) | func TestNow(t *testing.T) {
function TestFormatDate (line 35) | func TestFormatDate(t *testing.T) {
function TestCustomFormat (line 70) | func TestCustomFormat(t *testing.T) {
FILE: pkg/tplfunc/func.go
type Func (line 7) | type Func
function FuncMap (line 9) | func FuncMap(functions ...Func) template.FuncMap {
function init (line 19) | func init() {
FILE: pkg/tplfunc/math.go
function init (line 13) | func init() {
function Rand (line 17) | func Rand() Func {
FILE: pkg/tplfunc/math_test.go
function TestRand (line 10) | func TestRand(t *testing.T) {
function TestRandPanic (line 49) | func TestRandPanic(t *testing.T) {
FILE: pkg/tplfunc/string.go
function Repeat (line 18) | func Repeat() Func {
function Replace (line 26) | func Replace() Func {
function ToUpper (line 34) | func ToUpper() Func {
function ToLower (line 40) | func ToLower() Func {
function SnakeCase (line 46) | func SnakeCase() Func {
function CamelCase (line 54) | func CamelCase() Func {
function KebabCase (line 62) | func KebabCase() Func {
function Filenamify (line 70) | func Filenamify() Func {
FILE: pkg/tplfunc/string_test.go
function stringSlice (line 11) | func stringSlice(args []string) string {
function TestRepeat (line 19) | func TestRepeat(t *testing.T) {
function TestReplace (line 52) | func TestReplace(t *testing.T) {
function TestReplacePanic (line 86) | func TestReplacePanic(t *testing.T) {
function TestToUpper (line 109) | func TestToUpper(t *testing.T) {
function TestToLower (line 140) | func TestToLower(t *testing.T) {
function TestSnakeCase (line 171) | func TestSnakeCase(t *testing.T) {
function TestCamelCase (line 204) | func TestCamelCase(t *testing.T) {
function TestKebabCase (line 237) | func TestKebabCase(t *testing.T) {
FILE: pkg/utils/byte.go
type _byte (line 5) | type _byte struct
method FormatBinaryBytes (line 9) | func (b _byte) FormatBinaryBytes(n int64) string {
FILE: pkg/utils/cmd.go
type cmd (line 11) | type cmd struct
method StringEnumFlag (line 16) | func (cmd) StringEnumFlag(cmd *cobra.Command, p *string, name, shortha...
type enumValue (line 26) | type enumValue struct
method Set (line 31) | func (e *enumValue) Set(value string) error {
method String (line 39) | func (e *enumValue) String() string {
method Type (line 43) | func (e *enumValue) Type() string {
function isIncluded (line 47) | func isIncluded(value string, opts []string) bool {
function formatValuesForUsageDocs (line 56) | func formatValuesForUsageDocs(values []string) string {
FILE: pkg/validator/validator.go
function init (line 9) | func init() {
function Struct (line 13) | func Struct(s interface{}) error {
FILE: test/suite_test.go
function TestCommand (line 23) | func TestCommand(t *testing.T) {
function exec (line 48) | func exec(cmd *cobra.Command, args []string, success bool) {
FILE: test/testserver/testserver.go
function init (line 47) | func init() {
function Setup (line 55) | func Setup(ctx context.Context, rnd rand.Source) (account string, sessio...
function setupTestUser (line 68) | func setupTestUser(ctx context.Context, rnd *rand.Rand, account, session...
type testAuth (line 122) | type testAuth struct
method Phone (line 126) | func (t testAuth) Phone(_ context.Context) (string, error) { return...
method Password (line 127) | func (t testAuth) Password(_ context.Context) (string, error) { return...
method Code (line 128) | func (t testAuth) Code(_ context.Context, _ *tg.AuthSentCode) (string,...
method AcceptTermsOfService (line 132) | func (t testAuth) AcceptTermsOfService(_ context.Context, _ tg.HelpTer...
method SignUp (line 136) | func (t testAuth) SignUp(_ context.Context) (auth.UserInfo, error) {
Condensed preview — 234 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (582K chars).
[
{
"path": ".editorconfig",
"chars": 258,
"preview": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitesp"
},
{
"path": ".gitattributes",
"chars": 275,
"preview": "* text=auto eol=lf\n\n*.{png,jpg,jpeg,gif,webp,woff,woff2} binary\n\n# https://joshuatz.com/posts/2019/how-to-get-github-to-"
},
{
"path": ".github/CODEOWNERS",
"chars": 45,
"preview": "* @iyear\n*.go @XMLHexagram\n*.md @XMLHexagram\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
"chars": 1898,
"preview": "name: Bug Report\ndescription: Create a report to help us improve\ntitle: \"[Bug] Please complete title/请完善标题\"\nlabels: [\"bu"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 1037,
"preview": "name: Feature Request\ndescription: Suggest an idea for tdl\ntitle: \"[Feat] Please complete title/请完善标题\"\nlabels: [\"enhance"
},
{
"path": ".github/dependabot.yml",
"chars": 743,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".github/workflows/dependabot-fix.yml",
"chars": 737,
"preview": "name: dependabot-fix\n\non:\n pull_request:\n branches:\n - dependabot/go_modules/**\n push:\n branches:\n - d"
},
{
"path": ".github/workflows/docker.yml",
"chars": 2385,
"preview": "name: docker\n\non:\n workflow_dispatch:\n inputs:\n ref:\n description: 'Ref to checkout'\n required: t"
},
{
"path": ".github/workflows/docs.yml",
"chars": 1789,
"preview": "name: deploy docs\n\non:\n push:\n tags:\n - 'v*'\n workflow_dispatch:\n\npermissions:\n contents: read\n pages: write"
},
{
"path": ".github/workflows/master.yml",
"chars": 1950,
"preview": "name: master builder\n\non:\n pull_request:\n types: [opened, synchronize, reopened, labeled, unlabeled]\n branches: ["
},
{
"path": ".github/workflows/release.yml",
"chars": 1137,
"preview": "name: release\n\non:\n workflow_dispatch:\n push:\n tags:\n - 'v*'\n\npermissions:\n contents: write\n\njobs:\n homebrew"
},
{
"path": ".gitignore",
"chars": 70,
"preview": ".idea\nlog\n.tdl\n.vscode\ndownloads\n*.exe\ntdl\n.hugo_build.lock\n.DS_Store\n"
},
{
"path": ".golangci.yaml",
"chars": 814,
"preview": "version: \"2\"\nlinters:\n default: none\n enable:\n - exhaustive\n - goconst\n - govet\n - ineffassign\n - missp"
},
{
"path": ".goreleaser.yaml",
"chars": 1420,
"preview": "project_name: tdl\ndist: .tdl/dist\nenv:\n - GO111MODULE=on\nbuilds:\n - env:\n - CGO_ENABLED=0\n flags:\n - -tri"
},
{
"path": "Dockerfile",
"chars": 773,
"preview": "# https://www.docker.com/blog/faster-multi-platform-builds-dockerfile-cross-compilation-guide/\nFROM --platform=$BUILDPLA"
},
{
"path": "LICENSE",
"chars": 34523,
"preview": " GNU AFFERO GENERAL PUBLIC LICENSE\n Version 3, 19 November 2007\n\n Copyright (C)"
},
{
"path": "Makefile",
"chars": 282,
"preview": ".PHONY: build\nbuild:\n\tgoreleaser build --rm-dist --single-target --snapshot\n\t@echo \"go to '.tdl/dist' directory to see t"
},
{
"path": "README.md",
"chars": 1500,
"preview": "# tdl\n\n<img align=\"right\" src=\"docs/assets/img/logo.png\" height=\"280\" alt=\"\">\n\n> 📥 Telegram Downloader, but more than a "
},
{
"path": "README_zh.md",
"chars": 1293,
"preview": "> [!IMPORTANT]\n> 中文文档可能落后于英文文档,如果有问题请先查看英文文档。\n> 请使用英文发起新的 Issue, 以便于追踪和搜索\n\n# tdl\n\n<img align=\"right\" src=\"docs/assets/im"
},
{
"path": "app/chat/export.go",
"chars": 5225,
"preview": "package chat\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/expr-lang/expr\"\n\t\"github.com/fatih"
},
{
"path": "app/chat/export_enum.go",
"chars": 2847,
"preview": "// Code generated by go-enum DO NOT EDIT.\n// Version: 0.5.8\n// Revision: 3d844c8ecc59661ed7aa17bfd65727bc06a60ad8\n// Bui"
},
{
"path": "app/chat/ls.go",
"chars": 15447,
"preview": "package chat\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/expr-lang/expr\"\n\t\"github.c"
},
{
"path": "app/chat/ls_enum.go",
"chars": 2601,
"preview": "// Code generated by go-enum DO NOT EDIT.\n// Version: 0.5.8\n// Revision: 3d844c8ecc59661ed7aa17bfd65727bc06a60ad8\n// Bui"
},
{
"path": "app/chat/users.go",
"chars": 3771,
"preview": "package chat\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/go-faste"
},
{
"path": "app/dl/dl.go",
"chars": 5526,
"preview": "package dl\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/AlecAivazis/survey/v2\"\n\t\"github.com/fatih/color\"\n\t"
},
{
"path": "app/dl/elem.go",
"chars": 835,
"preview": "package dl\n\nimport (\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/gotd/td/telegram/peers\"\n\t\"github.com/gotd/td/tg\"\n\n\t\"github.com/iyear/tdl/"
},
{
"path": "app/dl/iter.go",
"chars": 11014,
"preview": "package dl\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"syn"
},
{
"path": "app/dl/iter_test.go",
"chars": 7178,
"preview": "package dl\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretc"
},
{
"path": "app/dl/progress.go",
"chars": 3084,
"preview": "package dl\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/fatih/color\"\n\t\"gi"
},
{
"path": "app/dl/serve.go",
"chars": 3799,
"preview": "package dl\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync\"\n\n\t\"github.com"
},
{
"path": "app/dl/serve.go.tmpl",
"chars": 730,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n <title>tdl serve(beta)</title>\n <style>\n body {\n display: flex;\n "
},
{
"path": "app/extension/extension.go",
"chars": 4056,
"preview": "package extension\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/go-fas"
},
{
"path": "app/forward/elem.go",
"chars": 830,
"preview": "package forward\n\nimport (\n\t\"github.com/gotd/td/telegram/peers\"\n\t\"github.com/gotd/td/tg\"\n\n\t\"github.com/iyear/tdl/core/for"
},
{
"path": "app/forward/forward.go",
"chars": 4872,
"preview": "package forward\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"github.com/expr-lang/expr\"\n\t\"github.com/expr-"
},
{
"path": "app/forward/iter.go",
"chars": 4509,
"preview": "package forward\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/expr-lang/expr/vm\"\n\t\"github.com/go-faster/errors\"\n"
},
{
"path": "app/forward/progress.go",
"chars": 2373,
"preview": "package forward\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\tpw \"github.com/jedib0t/go-pretty/v6/progress\"\n\t\""
},
{
"path": "app/internal/tctx/tctx.go",
"chars": 580,
"preview": "package tctx\n\nimport (\n\t\"context\"\n\n\t\"github.com/iyear/tdl/core/dcpool\"\n\t\"github.com/iyear/tdl/core/storage\"\n)\n\ntype kvKe"
},
{
"path": "app/login/code.go",
"chars": 2789,
"preview": "package login\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/AlecAivazis/survey/v2\"\n\t\"github.com/fatih/color\"\n\t\"github.co"
},
{
"path": "app/login/desktop.go",
"chars": 3589,
"preview": "package login\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\n\t\"github.com/AlecAivazis/survey/v2\"\n\t\"githu"
},
{
"path": "app/login/login.go",
"chars": 514,
"preview": "package login\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-faster/errors\"\n)\n\n//go:generate go-enum --values --names --flag --no"
},
{
"path": "app/login/login_enum.go",
"chars": 2462,
"preview": "// Code generated by go-enum DO NOT EDIT.\n// Version: 0.5.8\n// Revision: 3d844c8ecc59661ed7aa17bfd65727bc06a60ad8\n// Bui"
},
{
"path": "app/login/qr.go",
"chars": 2624,
"preview": "package login\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/AlecAivazis/survey/v2\"\n\t\"github.com/fatih/color\"\n\t\"gi"
},
{
"path": "app/migrate/backup.go",
"chars": 987,
"preview": "package migrate\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"os\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/go-faster/errors\"\n\t\""
},
{
"path": "app/migrate/migrate.go",
"chars": 897,
"preview": "package migrate\n\nimport (\n\t\"context\"\n\n\t\"github.com/AlecAivazis/survey/v2\"\n\t\"github.com/fatih/color\"\n\t\"github.com/go-fast"
},
{
"path": "app/migrate/recover.go",
"chars": 939,
"preview": "package migrate\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"os\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/go-faster/e"
},
{
"path": "app/up/elem.go",
"chars": 1000,
"preview": "package up\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gotd/td/telegram/message/entity\"\n\t\"github.com/gotd/td/telegram"
},
{
"path": "app/up/iter.go",
"chars": 5166,
"preview": "package up\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/expr-lang/expr/vm\"\n\t\"github.com/gabriel-vasile/mi"
},
{
"path": "app/up/progress.go",
"chars": 2273,
"preview": "package up\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/go-faster/errors\"\n\tpw \"github.com/jedi"
},
{
"path": "app/up/up.go",
"chars": 4190,
"preview": "package up\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"github.com/expr-lang/expr\"\n\t\"gith"
},
{
"path": "app/up/walk.go",
"chars": 1154,
"preview": "package up\n\nimport (\n\t\"io/fs\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/iyear/tdl/core/util/fsutil\"\n\t\"github.com/iyear/t"
},
{
"path": "cmd/chat.go",
"chars": 5111,
"preview": "package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gotd/contrib/middleware/ratelimit\"\n\t\"gi"
},
{
"path": "cmd/dl.go",
"chars": 3381,
"preview": "package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gotd/td/telegram\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/vip"
},
{
"path": "cmd/extension.go",
"chars": 3847,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\n\t\"github.com/go-faster/errors\"\n\t\"github.com/spf13/"
},
{
"path": "cmd/forward.go",
"chars": 1616,
"preview": "package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gotd/td/telegram\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.c"
},
{
"path": "cmd/gen.go",
"chars": 1675,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/go-faster/errors\"\n\t\"github.com/spf1"
},
{
"path": "cmd/login.go",
"chars": 1330,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/iyear/tdl/app"
},
{
"path": "cmd/migrate.go",
"chars": 1691,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/iyear/tdl/app/migrate\"\n\t\"github"
},
{
"path": "cmd/root.go",
"chars": 8640,
"preview": "package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-faster/er"
},
{
"path": "cmd/up.go",
"chars": 2230,
"preview": "package cmd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/gotd/td/telegram\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/iyear"
},
{
"path": "cmd/version.go",
"chars": 799,
"preview": "package cmd\n\nimport (\n\t\"bytes\"\n\t_ \"embed\"\n\t\"runtime\"\n\t\"text/template\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobr"
},
{
"path": "cmd/version.tmpl",
"chars": 108,
"preview": "Version: {{ .Version }}\nCommit: {{ .Commit }}\nDate: {{ .Date }}\n\n{{ .GoVersion }} {{ .GOOS }}/{{ .GOARCH }}\n"
},
{
"path": "core/dcpool/dcpool.go",
"chars": 2877,
"preview": "package dcpool\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/gotd/td/telegram\"\n\t\"github.com/gotd/td/tg\"\n\t\"go.uber.org/multi"
},
{
"path": "core/dcpool/middlewares.go",
"chars": 310,
"preview": "package dcpool\n\nimport (\n\t\"github.com/gotd/td/telegram\"\n\t\"github.com/gotd/td/tg\"\n)\n\nfunc chainMiddlewares(invoker tg.Inv"
},
{
"path": "core/downloader/downloader.go",
"chars": 2067,
"preview": "package downloader\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-faster/errors\"\n\t\"github.com/gotd/td/telegram/downloader\"\n\t\"go.u"
},
{
"path": "core/downloader/iter.go",
"chars": 320,
"preview": "package downloader\n\nimport (\n\t\"context\"\n\t\"io\"\n\n\t\"github.com/gotd/td/tg\"\n)\n\ntype Iter interface {\n\tNext(ctx context.Conte"
},
{
"path": "core/downloader/progress.go",
"chars": 1306,
"preview": "package downloader\n\nimport (\n\t\"time\"\n\n\t\"go.uber.org/atomic\"\n)\n\ntype Progress interface {\n\tOnAdd(elem Elem)\n\tOnDownload(e"
},
{
"path": "core/forwarder/clone.go",
"chars": 2411,
"preview": "package forwarder\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/go-faster/errors\"\n\t\"github.com/gotd/td/telegram/downloa"
},
{
"path": "core/forwarder/forwarder.go",
"chars": 12006,
"preview": "package forwarder\n\nimport (\n\t\"context\"\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/go-faster/errors\"\n\t\"github.com/gotd/td/bin\"\n\t\""
},
{
"path": "core/forwarder/forwarder_enum.go",
"chars": 2268,
"preview": "// Code generated by go-enum DO NOT EDIT.\n// Version: 0.5.8\n// Revision: 3d844c8ecc59661ed7aa17bfd65727bc06a60ad8\n// Bui"
},
{
"path": "core/forwarder/iter.go",
"chars": 414,
"preview": "package forwarder\n\nimport (\n\t\"context\"\n\n\t\"github.com/gotd/td/telegram/peers\"\n\t\"github.com/gotd/td/tg\"\n)\n\ntype Iter inter"
},
{
"path": "core/forwarder/progress.go",
"chars": 242,
"preview": "package forwarder\n\ntype ProgressClone interface {\n\tOnClone(elem Elem, state ProgressState)\n}\n\ntype Progress interface {\n"
},
{
"path": "core/go.mod",
"chars": 1763,
"preview": "module github.com/iyear/tdl/core\n\ngo 1.25.8\n\nrequire (\n\tgithub.com/cenkalti/backoff/v4 v4.3.0\n\tgithub.com/gabriel-vasile"
},
{
"path": "core/go.sum",
"chars": 9824,
"preview": "github.com/beevik/ntp v1.3.1 h1:Y/srlT8L1yQr58kyPWFPZIxRL8ttx2SRIpVYJqZIlAM=\ngithub.com/beevik/ntp v1.3.1/go.mod h1:fT6P"
},
{
"path": "core/logctx/logctx.go",
"chars": 444,
"preview": "package logctx\n\nimport (\n\t\"context\"\n\n\t\"go.uber.org/zap\"\n)\n\ntype ctxKey struct{}\n\nfunc From(ctx context.Context) *zap.Log"
},
{
"path": "core/middlewares/recovery/recovery.go",
"chars": 1507,
"preview": "package recovery\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n\t\"github.com/go-faster/errors\"\n\t\"github"
},
{
"path": "core/middlewares/retry/retry.go",
"chars": 1279,
"preview": "package retry\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/go-faster/errors\"\n\t\"github.com/gotd/td/bin\"\n\t\"github.com/gotd/td"
},
{
"path": "core/middlewares/takeout/middleware.go",
"chars": 661,
"preview": "package takeout\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/gotd/td/bin\"\n\t\"github.com/gotd/td/telegram\"\n\t\"github.com/go"
},
{
"path": "core/middlewares/takeout/takeout.go",
"chars": 831,
"preview": "package takeout\n\nimport (\n\t\"context\"\n\n\t\"github.com/gotd/td/tg\"\n)\n\nfunc Takeout(ctx context.Context, invoker tg.Invoker) "
},
{
"path": "core/storage/keygen/keygen.go",
"chars": 345,
"preview": "package keygen\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"sync\"\n)\n\nvar keyPool = sync.Pool{\n\tNew: func() interface{} {\n\t\tb := &byte"
},
{
"path": "core/storage/peers.go",
"chars": 2358,
"preview": "package storage\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"strconv\"\n\n\t\"github.com/gotd/td/telegram/peers\"\n\n\t\"gith"
},
{
"path": "core/storage/session.go",
"chars": 718,
"preview": "package storage\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/gotd/td/telegram\"\n\n\t\"github.com/iyear/tdl/core/storage/keyg"
},
{
"path": "core/storage/state.go",
"chars": 3575,
"preview": "package storage\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"strconv\"\n\n\t\"github.com/gotd/td/telegram/updates\"\n\n\t\"gi"
},
{
"path": "core/storage/storage.go",
"chars": 305,
"preview": "package storage\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-faster/errors\"\n)\n\ntype Storage interface {\n\tGet(ctx context.Contex"
},
{
"path": "core/tclient/tclient.go",
"chars": 3044,
"preview": "package tclient\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n\t\"github.com/go-faster/errors\"\n\t\""
},
{
"path": "core/tmedia/convert.go",
"chars": 3431,
"preview": "package tmedia\n\nimport (\n\t\"github.com/gotd/td/tg\"\n)\n\nfunc ConvInputMedia(media tg.MessageMediaClass) (tg.InputMediaClass"
},
{
"path": "core/tmedia/document.go",
"chars": 878,
"preview": "package tmedia\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/gabriel-vasile/mimetype\"\n\t\"github.com/gotd/td/tg\"\n)\n\nfunc GetDocumentI"
},
{
"path": "core/tmedia/media.go",
"chars": 1711,
"preview": "package tmedia\n\nimport (\n\t\"github.com/gotd/td/tg\"\n)\n\ntype Media struct {\n\tInputFileLoc tg.InputFileLocationClass // mtpr"
},
{
"path": "core/tmedia/photo.go",
"chars": 924,
"preview": "package tmedia\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/gotd/td/tg\"\n)\n\nfunc GetPhotoInfo(photo *tg.MessageMediaPhoto) (*Media,"
},
{
"path": "core/uploader/iter.go",
"chars": 384,
"preview": "package uploader\n\nimport (\n\t\"context\"\n\t\"io\"\n\n\t\"github.com/gotd/td/tg\"\n)\n\ntype Iter interface {\n\tNext(ctx context.Context"
},
{
"path": "core/uploader/progress.go",
"chars": 609,
"preview": "package uploader\n\nimport (\n\t\"context\"\n\n\t\"github.com/gotd/td/telegram/uploader\"\n)\n\ntype Progress interface {\n\tOnAdd(elem "
},
{
"path": "core/uploader/uploader.go",
"chars": 3839,
"preview": "package uploader\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/gabriel-vasile/mimetype\"\n\t\"github.com/go-faster/errors"
},
{
"path": "core/util/fsutil/fsutil.go",
"chars": 445,
"preview": "package fsutil\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nfunc GetNameWithoutExt(path string) string {\n\treturn strin"
},
{
"path": "core/util/logutil/logutil.go",
"chars": 642,
"preview": "package logutil\n\nimport (\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\n\t\"gopkg.in/natefinch/lumberjack.v2\"\n)\n\nfunc New("
},
{
"path": "core/util/mediautil/mediautil.go",
"chars": 1043,
"preview": "package mediautil\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/yapingcat/gomedia/go-mp4\"\n)\n\nfunc split(mime string) ("
},
{
"path": "core/util/netutil/netutil.go",
"chars": 636,
"preview": "package netutil\n\nimport (\n\t\"net/url\"\n\n\t\"github.com/go-faster/errors\"\n\t\"github.com/iyear/connectproxy\"\n\t\"golang.org/x/net"
},
{
"path": "core/util/tutil/device.go",
"chars": 258,
"preview": "package tutil\n\nimport \"github.com/gotd/td/telegram\"\n\nvar Device = telegram.DeviceConfig{\n\tDeviceModel: \"Desktop\",\n\tSy"
},
{
"path": "core/util/tutil/tutil.go",
"chars": 6156,
"preview": "package tutil\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/go-faster/errors\"\n\t\"github.com/"
},
{
"path": "docs/assets/_custom.scss",
"chars": 694,
"preview": "@import \"plugins/_scrollbars.scss\";\n\n.markdown pre {\n outline: none;\n}\n\n.command::before {\n content: attr(prompt);\n o"
},
{
"path": "docs/content/en/_index.md",
"chars": 1287,
"preview": "---\ntitle: Introduction\n---\n\n# tdl\n\n\n![]"
},
{
"path": "docs/content/en/getting-started/_index.md",
"chars": 66,
"preview": "---\ntitle: \"Getting Started\"\nbookFlatSection: true\nweight: 10\n---\n"
},
{
"path": "docs/content/en/getting-started/installation.md",
"chars": 7496,
"preview": "---\ntitle: \"Installation\"\nweight: 10\n---\n\n# Installation\n\n## One-Line Scripts\n\n{{< tabs \"scripts\" >}}\n\n{{< tab \"Windows\""
},
{
"path": "docs/content/en/getting-started/quick-start.md",
"chars": 1011,
"preview": "---\ntitle: \"Quick Start\"\nweight: 20\n---\n\n# Quick Start\n\n## Login\n\nWe don't specify the namespace here, so it will use th"
},
{
"path": "docs/content/en/getting-started/shell-completion.md",
"chars": 707,
"preview": "---\ntitle: \"Shell Completion\"\nweight: 30\n---\n\n# Shell Completion\n\nRun corresponding command to enable shell completion i"
},
{
"path": "docs/content/en/guide/_index.md",
"chars": 56,
"preview": "---\ntitle: \"Guide\"\nbookFlatSection: true\nweight: 20\n---\n"
},
{
"path": "docs/content/en/guide/download.md",
"chars": 4167,
"preview": "---\ntitle: \"Download\"\nweight: 30\n---\n\n# Download\n\n## From Links:\n\n{{< hint info >}}\nGet message links from \"Copy Link\" b"
},
{
"path": "docs/content/en/guide/extensions.md",
"chars": 5857,
"preview": "---\ntitle: \"Extensions 🆕\"\nweight: 70\n---\n\n# Extensions\n\n{{< hint warning >}}\nExtensions are a new feature in tdl. They a"
},
{
"path": "docs/content/en/guide/forward.md",
"chars": 4748,
"preview": "---\ntitle: \"Forward\"\nweight: 35\n---\n\n# Forward\n\nForward messages with automatic fallback and message routing\n\nOne-liner "
},
{
"path": "docs/content/en/guide/global-config.md",
"chars": 2976,
"preview": "---\ntitle: \"Global Config\"\nweight: 10\n---\n\n# Global Config\n\nGlobal config is some CLI flags that can be set in every com"
},
{
"path": "docs/content/en/guide/login.md",
"chars": 104,
"preview": "---\ntype: \"docs\"\ntitle: \"Login\"\nweight: 20\nbookHref: \"/getting-started/quick-start/#login\"\n---\n\n# Login\n"
},
{
"path": "docs/content/en/guide/migration.md",
"chars": 916,
"preview": "---\ntitle: \"Migration\"\nweight: 50\n---\n\n# Migration\n\nBackup or recover your data\n\n## Backup\n\nBackup all namespace data to"
},
{
"path": "docs/content/en/guide/template.md",
"chars": 5106,
"preview": "---\ntitle: \"Template Guide\"\nbookHidden: true\nbookToC: false\n---\n\n# Template Guide\n\nThis guide is intended to introduce v"
},
{
"path": "docs/content/en/guide/tools/_index.md",
"chars": 60,
"preview": "---\ntitle: \"Tools\"\nbookCollapseSection: true\nweight: 60\n---\n"
},
{
"path": "docs/content/en/guide/tools/export-members.md",
"chars": 661,
"preview": "---\ntitle: \"Export Members\"\nweight: 20\n---\n\n# Export Members\n\nExport chat members/subscribers, admins, bots, etc.\n\n{{< h"
},
{
"path": "docs/content/en/guide/tools/export-messages.md",
"chars": 2422,
"preview": "---\ntitle: \"Export Messages\"\nweight: 30\n---\n\n# Export Messages\n\nExport media messages from chats, channels, groups, etc."
},
{
"path": "docs/content/en/guide/tools/list-chats.md",
"chars": 628,
"preview": "---\ntitle: \"List Chats\"\nweight: 10\n---\n\n# List Chats\n\n## List all chats\n\n{{< command >}}\ntdl chat ls\n{{< /command >}}\n\n#"
},
{
"path": "docs/content/en/guide/upload.md",
"chars": 3768,
"preview": "---\ntitle: \"Upload\"\nweight: 40\n---\n\n# Upload\n\n## Upload Files\n\nUpload specified files and directories to `Saved Messages"
},
{
"path": "docs/content/en/more/_index.md",
"chars": 55,
"preview": "---\ntitle: \"More\"\nbookFlatSection: true\nweight: 30\n---\n"
},
{
"path": "docs/content/en/more/cli/_index.md",
"chars": 58,
"preview": "---\ntitle: \"CLI\"\nweight: 10\nbookHref: \"/more/cli/tdl\"\n---\n"
},
{
"path": "docs/content/en/more/data.md",
"chars": 166,
"preview": "---\ntitle: \"Data\"\nweight: 30\n---\n\n# Data\n\nYour account information will be stored in the `~/.tdl` directory.\n\nLog files "
},
{
"path": "docs/content/en/more/env.md",
"chars": 1076,
"preview": "---\ntitle: \"Env\"\nweight: 20\n---\n\n# Env\n\n{{< hint info >}}\nThe values of all environment variables have a lower priority "
},
{
"path": "docs/content/en/more/troubleshooting.md",
"chars": 1756,
"preview": "---\ntitle: \"Troubleshooting\"\nweight: 40\n---\n\n# Troubleshooting\n\n## Best Practices\n\nHow to minimize the risk of blocking?"
},
{
"path": "docs/content/en/reference/_index.md",
"chars": 25,
"preview": "---\nbookHidden: true\n---\n"
},
{
"path": "docs/content/en/reference/expr.md",
"chars": 396,
"preview": "---\ntitle: \"Expr Guide\"\nbookHidden: true\n---\n\n# Expr Guide\n\nExpr is powered by [expr](https://github.com/antonmedv/expr)"
},
{
"path": "docs/content/en/snippets/_index.md",
"chars": 25,
"preview": "---\nbookHidden: true\n---\n"
},
{
"path": "docs/content/en/snippets/chat.md",
"chars": 364,
"preview": "---\n---\n\n{{< details title=\"CHAT Examples\" open=false >}}\n\n#### Available Values:\n\n- `@iyear` (Username)\n- `iyear` (User"
},
{
"path": "docs/content/en/snippets/link.md",
"chars": 391,
"preview": "---\n---\n\n{{< details title=\"Message Link Examples\" open=false >}}\n\n- `https://t.me/telegram/193`\n- `https://t.me/c/16977"
},
{
"path": "docs/content/zh/_index.md",
"chars": 1034,
"preview": "---\ntitle: 介绍\n---\n\n# tdl\n\n\n导出需要聊天管理员权限。\n{{< "
},
{
"path": "docs/content/zh/guide/tools/export-messages.md",
"chars": 1817,
"preview": "---\ntitle: \"导出消息\"\nweight: 30\n---\n\n# 导出消息\n\n以 JSON 格式导出聊天、频道、群组等中的媒体消息。\n\n{{< include \"snippets/chat.md\" >}}\n\n{{< hint info"
},
{
"path": "docs/content/zh/guide/tools/list-chats.md",
"chars": 496,
"preview": "---\ntitle: \"列出聊天\"\nweight: 10\n---\n\n# 列出聊天\n\n## 列出所有聊天\n\n{{< command >}}\ntdl chat ls\n{{< /command >}}\n\n## JSON 格式\n\n{{< comma"
},
{
"path": "docs/content/zh/guide/upload.md",
"chars": 2880,
"preview": "---\ntitle: \"上传\"\nweight: 40\n---\n\n# 上传\n\n## 上传文件\n\n上传指定的文件和目录到 `保存的消息`:\n\n{{< command >}}\ntdl up -p /path/to/file -p /path/to"
},
{
"path": "docs/content/zh/more/_index.md",
"chars": 53,
"preview": "---\ntitle: \"更多\"\nbookFlatSection: true\nweight: 30\n---\n"
},
{
"path": "docs/content/zh/more/cli/_index.md",
"chars": 58,
"preview": "---\ntitle: \"命令行\"\nweight: 10\nbookHref: \"/more/cli/tdl\"\n---\n"
},
{
"path": "docs/content/zh/more/data.md",
"chars": 91,
"preview": "---\ntitle: \"数据\"\nweight: 30\n---\n\n# 数据\n\n您的帐户信息将存储在 `~/.tdl` 目录中。\n\n日志文件将存储在 `~/.tdl/log` 目录中。\n"
},
{
"path": "docs/content/zh/more/env.md",
"chars": 927,
"preview": "---\ntitle: \"环境变量\"\nweight: 20\n---\n\n# 环境变量\n\n{{< hint info >}}\n所有环境变量的值优先级低于命令行选项。\n{{< /hint >}}\n\n通过设置环境变量,避免在每次重复输入相同的命令行选"
},
{
"path": "docs/content/zh/more/troubleshooting.md",
"chars": 775,
"preview": "---\ntitle: \"疑难解答\"\nweight: 40\n---\n\n# 疑难解答\n\n## 最佳实践\n\n如何减小封号的风险?\n\n- 使用官方客户端会话登录。\n- 尽可能使用默认的下载和上传选项。不要设置过大的 `threads` 和 `siz"
},
{
"path": "docs/content/zh/reference/_index.md",
"chars": 25,
"preview": "---\nbookHidden: true\n---\n"
},
{
"path": "docs/content/zh/reference/expr.md",
"chars": 233,
"preview": "---\ntitle: \"表达式指南\"\nbookHidden: true\n---\n\n# 表达式指南\n\n表达式由 [expr](https://github.com/antonmedv/expr) 引擎提供支持,它是一个简单、轻量但功能强大的表"
},
{
"path": "docs/content/zh/snippets/_index.md",
"chars": 25,
"preview": "---\nbookHidden: true\n---\n"
},
{
"path": "docs/content/zh/snippets/chat.md",
"chars": 269,
"preview": "---\n---\n\n{{< details title=\"CHAT 示例\" open=false >}}\n\n#### 可用值:\n\n- `@iyear` (用户名)\n- `iyear` (无前缀 `@` 的用户名)\n- `123456789`("
},
{
"path": "docs/content/zh/snippets/link.md",
"chars": 351,
"preview": "---\n---\n\n{{< details title=\"消息链接示例\" open=false >}}\n\n- `https://t.me/telegram/193`\n- `https://t.me/c/1697797156/151`\n- `h"
},
{
"path": "docs/go.mod",
"chars": 132,
"preview": "module github.com/iyear/tdl/docs\n\ngo 1.25.8\n\nrequire github.com/alex-shpak/hugo-book v0.0.0-20230808113920-3f1bcccbfb24 "
},
{
"path": "docs/go.sum",
"chars": 237,
"preview": "github.com/alex-shpak/hugo-book v0.0.0-20230808113920-3f1bcccbfb24 h1:8NjMYBSFTtBLeT1VmpZAZznPOt1OH8aNCnE86sL4p4k=\ngithu"
},
{
"path": "docs/hugo.yaml",
"chars": 1320,
"preview": "baseURL: \"https://example.com/\"\ntitle: tdl\nenableGitInfo: true\ncanonifyURLs: true\n\nmodule:\n imports:\n - path: github"
},
{
"path": "docs/layouts/partials/docs/inject/footer.html",
"chars": 2686,
"preview": "<script>\n // hugo-book set all bookHref links to open in a new tab, this script will change it to open in the same ta"
},
{
"path": "docs/layouts/partials/docs/inject/head.html",
"chars": 1030,
"preview": "<script>\n var _hmt = _hmt || [];\n (function () {\n var hm = document.createElement(\"script\");\n hm.src"
},
{
"path": "docs/layouts/shortcodes/command.html",
"chars": 552,
"preview": "{{- $input := trim .Inner \" \\t\\r\\n\" -}}\n{{- $lines := split $input \"\\n\" -}}\n{{- $slash := false -}}\n\n<pre tabindex=\"0\">\n"
},
{
"path": "docs/layouts/shortcodes/image.html",
"chars": 274,
"preview": "{{ $file := .Get \"src\" }}\n{{ $height := .Get \"height\" }}\n{{ $width := .Get \"width\" }}\n{{ $align := .Get \"align\" }}\n\n{{ w"
},
{
"path": "docs/layouts/shortcodes/include.html",
"chars": 125,
"preview": "{{ $file := .Get 0 }}\n{{ with .Site.GetPage $file }}{{ .Content | replaceRE \"^---[\\\\s\\\\S]+?---\" \"\" | markdownify }}{{ en"
},
{
"path": "docs/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.content",
"chars": 13532,
"preview": "@charset \"UTF-8\";:root{--gray-100:#f8f9fa;--gray-200:#e9ecef;--gray-500:#adb5bd;--color-link:#0055bb;--color-visited-lin"
},
{
"path": "docs/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.json",
"chars": 188,
"preview": "{\"Target\":\"book.min.90a1b7a3a4485fcb940c779c3dfc891d6e9f3a078f4743cc4801844a72db8244.css\",\"MediaType\":\"text/css\",\"Data\":"
},
{
"path": "extension/extension.go",
"chars": 4081,
"preview": "package extension\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\n\t\"github.com/go-fast"
},
{
"path": "extension/go.mod",
"chars": 1760,
"preview": "module github.com/iyear/tdl/extension\n\ngo 1.25.8\n\nrequire (\n\tgithub.com/go-faster/errors v0.7.1\n\tgithub.com/gotd/td v0.1"
},
{
"path": "extension/go.sum",
"chars": 9414,
"preview": "github.com/beevik/ntp v1.3.1 h1:Y/srlT8L1yQr58kyPWFPZIxRL8ttx2SRIpVYJqZIlAM=\ngithub.com/beevik/ntp v1.3.1/go.mod h1:fT6P"
},
{
"path": "go.mod",
"chars": 4792,
"preview": "module github.com/iyear/tdl\n\ngo 1.25.8\n\nrequire (\n\tgithub.com/AlecAivazis/survey/v2 v2.3.7\n\tgithub.com/bcicen/jstream v1"
},
{
"path": "go.sum",
"chars": 28971,
"preview": "github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=\ngithub.com/AlecAivazis/survey/v2"
},
{
"path": "go.work",
"chars": 39,
"preview": "go 1.25.8\n\nuse (\n\t.\n\tcore\n\textension\n)\n"
},
{
"path": "go.work.sum",
"chars": 21418,
"preview": "cel.dev/expr v0.16.1 h1:NR0+oFYzR1CqLFhTAqg3ql59G9VfN8fKq1TCHJ6gq1g=\ncel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1R"
},
{
"path": "hack/lib.sh",
"chars": 662,
"preview": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n#### Convenient IO methods #####\nCOLOR_RED='\\033[0;31m'\nCOLOR_ORANGE='\\033[0;3"
},
{
"path": "hack/release_mod.sh",
"chars": 1100,
"preview": "#!/usr/bin/env bash\n\n# Examples:\n\n# Add tags to all submodules with version vX.Y.Z\n# ./hack/release_mod.sh tags v0.1.0\n\n"
},
{
"path": "main.go",
"chars": 722,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/signal\"\n\n\tsurveyterm \"github.com/AlecAivazis/survey/v2/terminal\"\n\t\"github.c"
},
{
"path": "pkg/clock/clock.go",
"chars": 833,
"preview": "package clock\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/beevik/ntp\"\n\t\"github.com/gotd/td/clock\"\n)\n\nconst defaultHost = \"poo"
},
{
"path": "pkg/consts/consts.go",
"chars": 484,
"preview": "package consts\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc init() {\n\tdir, err := os.UserHomeDir()\n\tif err != nil {\n\t\tpanic("
},
{
"path": "pkg/consts/flag.go",
"chars": 487,
"preview": "package consts\n\nconst (\n\tFlagStorage = \"storage\"\n\tFlagProxy = \"proxy\"\n\tFlagNamespace = \"ns\"\n\t"
},
{
"path": "pkg/consts/path.go",
"chars": 190,
"preview": "package consts\n\nvar (\n\tHomeDir string\n\tDataDir string\n\tLogPath string\n\tExtensionsPath "
},
{
"path": "pkg/consts/version.go",
"chars": 127,
"preview": "package consts\n\n// vars below are set by '-X' flag\nvar (\n\tVersion = \"dev\"\n\tCommit = \"unknown\"\n\tCommitDate = \"unkn"
},
{
"path": "pkg/extensions/extensions.go",
"chars": 889,
"preview": "package extensions\n\nimport (\n\t\"context\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n//go:generate go-enum --values --names --flag --n"
},
{
"path": "pkg/extensions/extensions_enum.go",
"chars": 2393,
"preview": "// Code generated by go-enum DO NOT EDIT.\n// Version: 0.5.8\n// Revision: 3d844c8ecc59661ed7aa17bfd65727bc06a60ad8\n// Bui"
},
{
"path": "pkg/extensions/extensions_test.go",
"chars": 604,
"preview": "package extensions\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestBaseExtension(t *testing.T) {"
},
{
"path": "pkg/extensions/github.go",
"chars": 2182,
"preview": "package extensions\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\n\t\"github.com/go-faster/e"
},
{
"path": "pkg/extensions/local.go",
"chars": 551,
"preview": "package extensions\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\ntype localExtension struct {\n\tbaseExtension\n}\n\nfunc (l *localExtension"
},
{
"path": "pkg/extensions/local_test.go",
"chars": 891,
"preview": "package extensions\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLocalExtension(t *"
},
{
"path": "pkg/extensions/manager.go",
"chars": 9649,
"preview": "package extensions\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"ru"
},
{
"path": "pkg/filterMap/filterMap.go",
"chars": 196,
"preview": "package filterMap\n\nfunc New(data []string, keyFn func(key string) string) map[string]struct{} {\n\tm := make(map[string]st"
},
{
"path": "pkg/key/key.go",
"chars": 203,
"preview": "package key\n\nimport (\n\t\"github.com/iyear/tdl/core/storage/keygen\"\n)\n\nfunc App() string {\n\treturn keygen.New(\"app\")\n}\n\nfu"
},
{
"path": "pkg/kv/bolt.go",
"chars": 3529,
"preview": "package kv\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\n\t\"github.com/go-faster/errors\"\n\t\"github.com/mitchellh/mapstructure\""
},
{
"path": "pkg/kv/file.go",
"chars": 3299,
"preview": "package kv\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\n\t\"github.com/go-faster/errors\"\n\t\"github"
},
{
"path": "pkg/kv/kv.go",
"chars": 1371,
"preview": "package kv\n\nimport (\n\t\"context\"\n\t\"io\"\n\n\t\"github.com/go-faster/errors\"\n\n\t\"github.com/iyear/tdl/core/storage\"\n)\n\n//go:gene"
},
{
"path": "pkg/kv/kv_enum.go",
"chars": 2183,
"preview": "// Code generated by go-enum DO NOT EDIT.\n// Version: 0.5.8\n// Revision: 3d844c8ecc59661ed7aa17bfd65727bc06a60ad8\n// Bui"
},
{
"path": "pkg/kv/kv_test.go",
"chars": 3480,
"preview": "package kv\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/s"
},
{
"path": "pkg/kv/legacy.go",
"chars": 3483,
"preview": "package kv\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/go-faster/errors\"\n\t\"github.com/mitchellh/mapstructure\"\n\t\"go."
},
{
"path": "pkg/prog/prog.go",
"chars": 1496,
"preview": "package prog\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/jedib0t/go-pretty/v6/progress\"\n\t\"githu"
},
{
"path": "pkg/prog/tracker.go",
"chars": 869,
"preview": "package prog\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/jedib0t/go-pretty/v6/progress\"\n\n\t\"github.com/iyear/td"
},
{
"path": "pkg/ps/ps.go",
"chars": 1207,
"preview": "package ps\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\n\t\"github.com/shirou/gopsutil/v3/process\"\n\n\t\"github.com/iyear/td"
},
{
"path": "pkg/tclient/app.go",
"chars": 476,
"preview": "package tclient\n\nconst (\n\tAppBuiltin = \"builtin\"\n\tAppDesktop = \"desktop\"\n)\n\ntype App struct {\n\tAppID int\n\tAppHash stri"
},
{
"path": "pkg/tclient/tclient.go",
"chars": 1228,
"preview": "package tclient\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-faster/errors\"\n\t\"github.com/gotd/td/telegram\"\n\n\t\"gi"
},
{
"path": "pkg/tdesktop/.s",
"chars": 0,
"preview": ""
}
]
// ... and 34 more files (download for full content)
About this extraction
This page contains the full source code of the iyear/tdl GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 234 files (517.4 KB), approximately 177.4k tokens, and a symbol index with 695 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.