├── .github └── workflows │ └── build-go.yml ├── .gitignore ├── CloudFlare.md ├── Dockerfile ├── LICENSE ├── README.md ├── README_en.md ├── api └── vercel.go ├── assets ├── assets.go └── templates │ ├── files.tmpl │ ├── footer.tmpl │ ├── header.tmpl │ ├── images.tmpl │ └── pwd.tmpl ├── conf └── conf.go ├── control └── control.go ├── get.sh ├── go.mod ├── go.sum ├── main.go ├── utils └── utils.go └── vercel.json /.github/workflows/build-go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | release: 5 | types: [ "created" ] 6 | 7 | jobs: 8 | build_and_upload_assets: 9 | permissions: write-all 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v3 17 | with: 18 | go-version: 1.17.2 19 | 20 | - name: Update go.mod to use Go 1.17 21 | run: | 22 | sed -i 's/go 1.20/go 1.17/' go.mod 23 | 24 | - name: Download dependencies 25 | run: go mod tidy 26 | 27 | - name: Build Linux arm64 28 | run: | 29 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o tgState main.go 30 | 31 | - name: Zip Linux amd64 32 | run: | 33 | sudo apt-get install -y zip 34 | zip tgState_arm64.zip tgState 35 | 36 | - name: Delete tgState arm64 37 | run: | 38 | rm tgState 39 | 40 | - name: Build Linux amd64 41 | run: | 42 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o tgState main.go 43 | 44 | - name: Zip Linux amd64 45 | run: | 46 | sudo apt-get install -y zip 47 | zip tgState.zip tgState 48 | 49 | - name: Upload server asset 50 | uses: actions/upload-release-asset@v1 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | with: 54 | upload_url: ${{ github.event.release.upload_url }} 55 | asset_path: ./tgState.zip 56 | asset_name: tgState.zip 57 | asset_content_type: application/zip 58 | 59 | - name: Upload server asset 60 | uses: actions/upload-release-asset@v1 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | with: 64 | upload_url: ${{ github.event.release.upload_url }} 65 | asset_path: ./tgState_arm64.zip 66 | asset_name: tgState_arm64.zip 67 | asset_content_type: application/zip 68 | 69 | - name: Set up Docker Buildx 70 | uses: docker/setup-buildx-action@v1 71 | 72 | - name: Log in to Docker Hub 73 | uses: docker/login-action@v1 74 | with: 75 | username: ${{ secrets.DOCKERHUB_USERNAME }} 76 | password: ${{ secrets.DOCKERHUB_TOKEN }} 77 | 78 | - name: Build and push Docker image 79 | uses: docker/build-push-action@v2 80 | with: 81 | context: . 82 | push: true 83 | tags: csznet/tgstate:latest 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> Go 2 | # If you prefer the allow list template instead of the deny list, see community template: 3 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 4 | # 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | # Test binary, built with `go test -c` 12 | *.test 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | # Go workspace file 18 | go.work 19 | #diy 20 | main 21 | tgstate 22 | mk.txt 23 | .idea/* 24 | .vscode/* 25 | tmp/* 26 | .air.toml 27 | .vercel 28 | -------------------------------------------------------------------------------- /CloudFlare.md: -------------------------------------------------------------------------------- 1 | 进阶指南 2 | == 3 | 4 | Vercel 5 | -- 6 | 7 | vercel默认域名部分地区会有阻断,建议添加自定义域名 8 | 使用cname解析到```cname-china.vercel-dns.com``` 9 | 10 | CloudFlare 11 | -- 12 | **SSL证书访问** 13 | 目的:解决开启SSL&Cloudflare CDN后重定向过多问题 14 | 设置路径:域名->SSL/TLS->Overview 15 | 设置为Full(strict) 16 | ![SSL证书访问](https://img-static.csz.net/d/BQACAgUAAxkDAAMUZSV2Wggiieo9_XSgODTLhW6fg-UAAukLAAKrzjBVQ0hH_g6a9OUwBA) 17 | 18 | **完全缓存图片** 19 | 目的:加快访问速度,减少api请求次数 20 | 设置路径:域名->Rules->Page Rules->Create Page Rule 21 | 给```/d/*```设置缓存所有,如下 22 | ![完全缓存图片](https://img-static.csz.net/d/BQACAgUAAxkDAAMVZSV2jVzUitEjGJz_GjZwprJ-nV8AAuoLAAKrzjBV7g9PeEBhKrkwBA) 23 | ![完全缓存图片](https://img-static.csz.net/d/BQACAgUAAxkDAAMXZSV2yzXwcPXgwuRctimd5_EDWq8AAuwLAAKrzjBVuAQYIFm1Sv4wBA) 24 | 25 | **控制请求速率** 26 | 目的:防止刷上传 27 | 设置路径:域名->Security->WAF->Rate limiting rules 28 | 建议给```/api```限制在10s不超过2次请求,如下 29 | ![控制请求速率](https://img-static.csz.net/d/BQACAgUAAxkDAAMWZSV2nJe5fOA6DZsdez4DAAG_MWbEAALrCwACq84wVaOhPWnmR--HMAQ) 30 | 31 | **开启Always Online** 32 | 目的:当服务宕机后,图片正常访问 33 | 设置路径:域名->Caching->Configuration->Always Online 34 | ![开启Always Online](https://img-static.csz.net/d/BQACAgUAAxkDAAMYZSV23bs8YRmChLhrs2BLwUWsRZ4AAu4LAAKrzjBVHlJjirBp9hgwBA) -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用官方的 Ubuntu 基础镜像 2 | FROM ubuntu:latest 3 | 4 | # 安装 ca-certificates 包,用于更新根证书 5 | RUN apt-get update && apt-get install -y ca-certificates 6 | 7 | # 将编译好的 server 和 client 二进制文件复制到容器中 8 | COPY tgState /app/tgState 9 | 10 | # 设置工作目录 11 | WORKDIR /app 12 | 13 | # 设置暴露的端口 14 | EXPOSE 8088 15 | 16 | # 设置容器启动时要执行的命令 17 | CMD [ "/app/tgState" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 csznet 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 | tgState 2 | == 3 | 4 | [English](https://github.com/csznet/tgState/blob/main/README_en.md) 5 | 6 | 一款以Telegram作为储存的文件外链系统 7 | 8 | 不限制文件大小和格式 9 | 10 | 可以作为telegram图床,也可以作为telegram网盘使用。 11 | 12 | 支持web上传文件和telegram直接上传 13 | 14 | 搭配CLoudFlare使用:https://github.com/csznet/tgState/blob/main/CloudFlare.md 15 | 16 | 如有疑惑,可以咨询TG @tgstate123 17 | 18 | # 演示 19 | 20 | https://tgstate.vercel.app 21 | 22 | 搭建在vercel,资源限制,大于5MB的文件不支持 23 | 24 | 演示图片: 25 | 26 | ![tgState](https://tgstate.vercel.app/d/BQACAgUAAx0EcyK3ugACByxlOR-Nfl4esavoO4zdaYIP_k1KYQACDAsAAkf4yFVpf_awaEkS8jAE) 27 | 28 | # 参数说明 29 | 30 | 必填参数 31 | 32 | - target 33 | - token 34 | 35 | 可选参数 36 | 37 | - pass 38 | - mode 39 | - url 40 | - port 41 | 42 | ## target 43 | 44 | 目标可为频道、群组、个人 45 | 46 | 当目标为频道时,需要将Bot拉入频道作为管理员,公开频道并自定义频道Link,target值填写Link,如@xxxx 47 | 48 | 当目标为群组时,需要将Bot拉入群组,公开群组并自定义群组Link,target值填写Link,如@xxxx 49 | 50 | 当目标为个人时,则为telegram id(@getmyid_bot获取) 51 | 52 | ## token 53 | 54 | 填写你的bot token 55 | 56 | ## pass 57 | 58 | 填写访问密码,如不需要,直接填写```none```即可 59 | 60 | ## mode 61 | 62 | - ```p``` 代表网盘模式运行,不限制上传后缀 63 | - ```m``` 在p模式的基础上关闭网页上传,可私聊进行上传(如果target是个人,则只支持指定用户进行私聊上传 64 | 65 | ## url 66 | 67 | bot获取FileID的前置域名地址自动补充及api返回完整url的补充 68 | 69 | ## port 70 | 71 | 自定义运行端口 72 | 73 | # 管理 74 | 75 | ## 获取FIleID 76 | 77 | 对bot聊天中的文件引用并回复```get```可以获取FileID,搭建地址+获取的path即可访问资源 78 | 79 | 如果配置了url参数,会直接返回完整的地址 80 | 81 | ![image](https://github.com/csznet/tgState/assets/127601663/5b1fd6c0-652c-41de-bb63-e2f20b257022) 82 | 83 | # 部署 84 | 85 | ## 二进制 86 | 87 | Linux amd64下载 88 | 89 | ``` 90 | wget https://github.com/csznet/tgState/releases/latest/download/tgState.zip && unzip tgState.zip && rm tgState.zip 91 | ``` 92 | 93 | Linux arm64下载 94 | 95 | ``` 96 | wget https://github.com/csznet/tgState/releases/latest/download/tgState_arm64.zip && unzip tgState_arm64.zip && rm tgState_arm64.zip 97 | ``` 98 | 99 | Linux 一键脚本 100 | 101 | ``` 102 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/csznet/tgState/main/get.sh)" 103 | ``` 104 | 105 | 106 | **使用方法** 107 | 108 | ``` 109 | ./tgState 参数 110 | ``` 111 | 112 | **例子** 113 | ``` 114 | ./tgState -token xxxx -target @xxxx 115 | ``` 116 | 117 | **后台运行** 118 | 119 | ``` 120 | nohup ./tgState 参数 & 121 | ``` 122 | 123 | ## Docker 124 | 125 | pull镜像 126 | ``` 127 | docker pull csznet/tgstate:latest 128 | ``` 129 | 130 | 启动 131 | ``` 132 | docker run -d -p 8088:8088 --name tgstate 参数 --net=host csznet/tgstate:latest 133 | ``` 134 | 其中docker的参数需要设置为环境变量 135 | 136 | 开机自启需要加上 137 | ``` 138 | --restart always 139 | ``` 140 | 141 | 142 | **例子** 143 | ``` 144 | docker run -d -p 8088:8088 --name tgstate -e token=token -e target=@target -e mode=p --net=host csznet/tgstate:latest 145 | ``` 146 | 147 | ## Vercel 148 | 149 | 不支持大于5mb文件,不支持tg获取文件路径 150 | 151 | [点我传送至Vercel配置页面](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcsznet%2FtgState&env=token&env=target&env=pass&env=mode&env=url&project-name=tgState&repository-name=tgState) 152 | 153 | # API说明 154 | 155 | POST方法到路径```/api``` 156 | 157 | 表单传输,字段名为image,内容为二进制数据 158 | 159 | 当设置访问密码时,直接将密码加入url参数pass中,如密码为123: 160 | 161 | ``` 162 | /api?pass=123 163 | ``` 164 | 165 | 返回示例: 166 | 167 | ```json 168 | {"code": 1, "message": "/d/xxx","url":"xxx"} 169 | ``` 170 | 171 | json格式的`url`默认返回tgState的`url`参数+访问路径,如果只得到了路径则需要自行设置`url`参数 172 | 173 | picgo-plugin-web-uploader填写说明: 174 | 175 | POST参数名:`image` 176 | 177 | JSON路径:`url` 178 | 179 | ![image](https://github.com/csznet/tgState/assets/127601663/d70e6a42-1f21-4cbb-8ba5-1e9f7d9660a4) 180 | 181 | 182 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # tgState 2 | == 3 | 4 | [中文](https://github.com/csznet/tgState/blob/main/README.md) 5 | 6 | A file external link system using Telegram as storage. 7 | 8 | No restrictions on file size and format. 9 | 10 | Can be used as a Telegram image hosting service or a Telegram cloud drive. 11 | 12 | Supports web and Telegram direct file uploads. 13 | 14 | Use with CloudFlare: https://github.com/csznet/tgState/blob/main/CloudFlare.md 15 | 16 | For any questions, consult TG @tgstate123 17 | 18 | # Demo 19 | 20 | https://tgstate.vercel.app / https://tgstate.ikun123.com/ 21 | 22 | Hosted on Vercel, resource limitations - files larger than 5MB are not supported. 23 | 24 | Demo image: 25 | 26 | ![tgState](https://tgstate.vercel.app/d/BQACAgUAAx0EcyK3ugACByxlOR-Nfl4esavoO4zdaYIP_k1KYQACDAsAAkf4yFVpf_awaEkS8jAE) 27 | 28 | # Parameter Description 29 | 30 | Mandatory parameters: 31 | 32 | - target 33 | - token 34 | 35 | Optional parameters: 36 | 37 | - pass 38 | - mode 39 | - url 40 | - port 41 | 42 | ## target 43 | 44 | The target can be a channel, group, or individual. 45 | 46 | When the target is a channel, the bot needs to be added to the channel as an administrator, make the channel public, and customize the channel link. The target value should be filled with the link, such as @xxxx. 47 | 48 | When the target is a group, the bot needs to be added to the group, make the group public, and customize the group link. The target value should be filled with the link, such as @xxxx. 49 | 50 | When the target is an individual, it is the Telegram ID (obtained from @getmyid_bot). 51 | 52 | ## token 53 | 54 | Fill in your bot token. 55 | 56 | ## pass 57 | 58 | Fill in the access password. If not needed, fill in ```none``` directly. 59 | 60 | ## mode 61 | 62 | - ```p``` represents running in cloud drive mode, with no restriction on uploaded suffixes. 63 | - ```m``` On top of the p mode, web upload is disabled, and upload can be done via private chat (if the target is an individual, only specified users can upload via private chat). 64 | 65 | ## url 66 | 67 | The pre-domain address that bot obtains FileID is automatically filled in. 68 | 69 | ## port 70 | 71 | Customize the running port. 72 | 73 | # Management 74 | 75 | ## Get FIleID 76 | 77 | Replying with ```get``` to the file reference in the bot's chat can get the FileID. Access the resource by combining the built address and the obtained path. 78 | 79 | If the url parameter is configured, the complete address will be returned directly. 80 | 81 | ![image](https://github.com/csznet/tgState/assets/127601663/5b1fd6c0-652c-41de-bb63-e2f20b257022) 82 | 83 | # Deployment 84 | 85 | ## Binary 86 | 87 | Download for Linux amd64 88 | 89 | ``` 90 | wget https://github.com/csznet/tgState/releases/latest/download/tgState.zip && unzip tgState.zip && rm tgState.zip 91 | ``` 92 | 93 | Download for Linux arm64 94 | 95 | ``` 96 | wget https://github.com/csznet/tgState/releases/latest/download/tgState_arm64.zip && unzip tgState_arm64.zip && rm tgState_arm64.zip 97 | ``` 98 | 99 | **Usage** 100 | 101 | ```./tgState parameters``` 102 | 103 | **Example** 104 | 105 | ```./tgState -token xxxx -target @xxxx``` 106 | 107 | **Run in the background** 108 | 109 | ```nohup ./tgState parameters &``` 110 | 111 | ## Docker 112 | 113 | Pull the image 114 | 115 | ```docker pull csznet/tgstate:latest``` 116 | 117 | 118 | Start 119 | 120 | ``` 121 | docker run -d -p 8088:8088 --name tgstate parameters --net=host csznet/tgstate:latest 122 | ``` 123 | 124 | Where docker parameters need to be set as environment variables. 125 | 126 | **Example** 127 | 128 | ``` 129 | docker run -d -p 8088:8088 --name tgstate -e token=aaa -e target=@bbb --net=host csznet/tgstate:latest 130 | ``` 131 | 132 | ## Vercel 133 | 134 | Does not support files larger than 5MB and does not support Telegram in getting file paths. 135 | 136 | [Click here to go to the Vercel configuration page](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcsznet%2FtgState&env=token&env=target&env=pass&env=mode&project-name=tgState&repository-name=tgState) 137 | 138 | # API Description 139 | 140 | POST method to the path ```/api``` 141 | 142 | Form transmission, field name is image, content is binary data. 143 | -------------------------------------------------------------------------------- /api/vercel.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "strings" 7 | 8 | "csz.net/tgstate/conf" 9 | "csz.net/tgstate/control" 10 | ) 11 | 12 | func Vercel(w http.ResponseWriter, r *http.Request) { 13 | conf.BotToken = os.Getenv("token") 14 | conf.ChannelName = os.Getenv("target") 15 | conf.Pass = os.Getenv("pass") 16 | conf.Mode = os.Getenv("mode") 17 | conf.BaseUrl = os.Getenv("url") 18 | // 获取请求路径 19 | path := r.URL.Path 20 | // 如果请求路径以 "/img/" 开头 21 | if strings.HasPrefix(path, conf.FileRoute) { 22 | control.D(w, r) 23 | return // 结束处理,确保不执行默认处理 24 | } 25 | switch path { 26 | case "/api": 27 | // 调用 control 包中的 UploadImageAPI 处理函数 28 | control.Middleware(control.UploadImageAPI)(w, r) 29 | case "/pwd": 30 | control.Pwd(w, r) 31 | default: 32 | control.Middleware(control.Index)(w, r) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /assets/assets.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import "embed" 4 | 5 | var ( 6 | //go:embed templates 7 | Templates embed.FS 8 | ) 9 | -------------------------------------------------------------------------------- /assets/templates/files.tmpl: -------------------------------------------------------------------------------- 1 | {{template "public/header" .}} 2 |

上传文件到 Telegram

4 |
上传中...
5 |
6 | {{template "public/footer" .}} 7 | -------------------------------------------------------------------------------- /assets/templates/footer.tmpl: -------------------------------------------------------------------------------- 1 | {{define "public/footer"}} 2 | 199 | 202 | 211 | 212 | 213 | 214 | 215 | {{end}} -------------------------------------------------------------------------------- /assets/templates/header.tmpl: -------------------------------------------------------------------------------- 1 | {{define "public/header"}} 2 | 3 | 4 | 5 | 6 | tgState 7 | 9 | 10 | 11 | 149 | 150 | 151 | 152 | {{end}} -------------------------------------------------------------------------------- /assets/templates/images.tmpl: -------------------------------------------------------------------------------- 1 | {{template "public/header" .}} 2 |

上传图片到 Telegram

5 |
上传中...
6 |
7 | {{template "public/footer" .}} -------------------------------------------------------------------------------- /assets/templates/pwd.tmpl: -------------------------------------------------------------------------------- 1 | {{template "public/header" .}} 2 |

Powered by tgState

-------------------------------------------------------------------------------- /conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | var BotToken string 4 | var ChannelName string 5 | var Pass string 6 | var Mode string 7 | var BaseUrl string 8 | 9 | type UploadResponse struct { 10 | Code int `json:"code"` 11 | Message string `json:"message"` 12 | ImgUrl string `json:"url"` 13 | } 14 | 15 | const FileRoute = "/d/" 16 | -------------------------------------------------------------------------------- /control/control.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | import ( 4 | "encoding/json" 5 | "html/template" 6 | "io" 7 | "log" 8 | "net/http" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "csz.net/tgstate/assets" 15 | "csz.net/tgstate/conf" 16 | "csz.net/tgstate/utils" 17 | ) 18 | 19 | // UploadImageAPI 上传图片api 20 | func UploadImageAPI(w http.ResponseWriter, r *http.Request) { 21 | w.Header().Set("Access-Control-Allow-Origin", "*") 22 | if r.Method == http.MethodPost { 23 | // 获取上传的文件 24 | file, header, err := r.FormFile("image") 25 | if err != nil { 26 | errJsonMsg("Unable to get file", w) 27 | // http.Error(w, "Unable to get file", http.StatusBadRequest) 28 | return 29 | } 30 | defer file.Close() 31 | if conf.Mode != "p" && r.ContentLength > 20*1024*1024 { 32 | // 检查文件大小 33 | errJsonMsg("File size exceeds 20MB limit", w) 34 | return 35 | } 36 | // 检查文件类型 37 | allowedExts := []string{".jpg", ".jpeg", ".png"} 38 | ext := filepath.Ext(header.Filename) 39 | valid := false 40 | for _, allowedExt := range allowedExts { 41 | if ext == allowedExt { 42 | valid = true 43 | break 44 | } 45 | } 46 | if conf.Mode != "p" && !valid { 47 | errJsonMsg("Invalid file type. Only .jpg, .jpeg, and .png are allowed.", w) 48 | // http.Error(w, "Invalid file type. Only .jpg, .jpeg, and .png are allowed.", http.StatusBadRequest) 49 | return 50 | } 51 | res := conf.UploadResponse{ 52 | Code: 0, 53 | Message: "error", 54 | } 55 | img := conf.FileRoute + utils.UpDocument(utils.TgFileData(header.Filename, file)) 56 | if img != conf.FileRoute { 57 | res = conf.UploadResponse{ 58 | Code: 1, 59 | Message: img, 60 | ImgUrl: strings.TrimSuffix(conf.BaseUrl, "/") + img, 61 | } 62 | } 63 | w.Header().Set("Content-Type", "application/json") 64 | w.WriteHeader(http.StatusOK) 65 | json.NewEncoder(w).Encode(res) 66 | return 67 | } 68 | 69 | // 如果不是POST请求,返回错误响应 70 | http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) 71 | } 72 | func errJsonMsg(msg string, w http.ResponseWriter) { 73 | // 这里示例直接返回JSON响应 74 | response := conf.UploadResponse{ 75 | Code: 0, 76 | Message: msg, 77 | } 78 | w.Header().Set("Content-Type", "application/json") 79 | w.WriteHeader(http.StatusOK) 80 | json.NewEncoder(w).Encode(response) 81 | } 82 | func D(w http.ResponseWriter, r *http.Request) { 83 | path := r.URL.Path 84 | id := strings.TrimPrefix(path, conf.FileRoute) 85 | if id == "" { 86 | // 设置响应的状态码为 404 87 | w.WriteHeader(http.StatusNotFound) 88 | // 写入响应内容 89 | w.Write([]byte("404 Not Found")) 90 | return 91 | } 92 | 93 | // 发起HTTP GET请求来获取Telegram图片 94 | fileUrl, _ := utils.GetDownloadUrl(id) 95 | resp, err := http.Get(fileUrl) 96 | if err != nil { 97 | http.Error(w, "Failed to fetch content", http.StatusInternalServerError) 98 | return 99 | } 100 | w.Header().Set("Content-Disposition", "inline") // 设置为 "inline" 以支持在线播放 101 | // 检查Content-Type是否为图片类型 102 | if !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/octet-stream") { 103 | w.WriteHeader(http.StatusNotFound) 104 | w.Write([]byte("404 Not Found")) 105 | return 106 | } 107 | contentLength, err := strconv.Atoi(resp.Header.Get("Content-Length")) 108 | if err != nil { 109 | log.Println("获取Content-Length出错:", err) 110 | return 111 | } 112 | buffer := make([]byte, contentLength) 113 | n, err := resp.Body.Read(buffer) 114 | defer resp.Body.Close() 115 | if err != nil && err != io.ErrUnexpectedEOF { 116 | log.Println("读取响应主体数据时发生错误:", err) 117 | return 118 | } 119 | // 输出文件内容到控制台 120 | if string(buffer[:12]) == "tgstate-blob" { 121 | content := string(buffer) 122 | lines := strings.Split(content, "\n") 123 | log.Println("分块文件:" + lines[1]) 124 | var fileSize string 125 | var startLine = 2 126 | if strings.HasPrefix(lines[2], "size") { 127 | fileSize = lines[2][len("size"):] 128 | startLine = startLine + 1 129 | } 130 | w.Header().Set("Content-Type", "application/octet-stream") 131 | w.Header().Set("Content-Disposition", "attachment; filename=\""+lines[1]+"\"") 132 | w.Header().Set("Content-Length", fileSize) 133 | for i := startLine; i < len(lines); i++ { 134 | fileStatus := false 135 | var fileUrl string 136 | var reTry = 0 137 | for !fileStatus { 138 | if reTry > 0 { 139 | time.Sleep(5 * time.Second) 140 | } 141 | reTry = reTry + 1 142 | fileUrl, fileStatus = utils.GetDownloadUrl(strings.ReplaceAll(lines[i], " ", "")) 143 | } 144 | blobResp, err := http.Get(fileUrl) 145 | if err != nil { 146 | http.Error(w, "Failed to fetch content", http.StatusInternalServerError) 147 | return 148 | } 149 | _, err = io.Copy(w, blobResp.Body) 150 | blobResp.Body.Close() 151 | if err != nil { 152 | log.Println("写入响应主体数据时发生错误:", err) 153 | return 154 | } 155 | } 156 | } else { 157 | // 使用DetectContentType函数检测文件类型 158 | w.Header().Set("Content-Type", http.DetectContentType(buffer)) 159 | _, err = w.Write(buffer[:n]) 160 | if err != nil { 161 | http.Error(w, "Failed to write content", http.StatusInternalServerError) 162 | log.Println(http.StatusInternalServerError) 163 | return 164 | } 165 | _, err = io.Copy(w, resp.Body) 166 | resp.Body.Close() 167 | if err != nil { 168 | log.Println(http.StatusInternalServerError) 169 | return 170 | } 171 | } 172 | } 173 | 174 | // Index 首页 175 | func Index(w http.ResponseWriter, r *http.Request) { 176 | htmlPath := "templates/images.tmpl" 177 | if conf.Mode == "p" { 178 | htmlPath = "templates/files.tmpl" 179 | } 180 | file, err := assets.Templates.ReadFile(htmlPath) 181 | if err != nil { 182 | http.Error(w, "HTML file not found", http.StatusNotFound) 183 | return 184 | } 185 | // 读取头部模板 186 | headerFile, err := assets.Templates.ReadFile("templates/header.tmpl") 187 | if err != nil { 188 | http.Error(w, "Header template not found", http.StatusNotFound) 189 | return 190 | } 191 | 192 | // 读取页脚模板 193 | footerFile, err := assets.Templates.ReadFile("templates/footer.tmpl") 194 | if err != nil { 195 | http.Error(w, "Footer template not found", http.StatusNotFound) 196 | return 197 | } 198 | 199 | // 创建HTML模板并包括头部 200 | tmpl := template.New("html") 201 | tmpl, err = tmpl.Parse(string(headerFile)) 202 | if err != nil { 203 | http.Error(w, "Error parsing header template", http.StatusInternalServerError) 204 | return 205 | } 206 | 207 | // 包括主HTML内容 208 | tmpl, err = tmpl.Parse(string(file)) 209 | if err != nil { 210 | http.Error(w, "Error parsing HTML template", http.StatusInternalServerError) 211 | return 212 | } 213 | 214 | // 包括页脚 215 | tmpl, err = tmpl.Parse(string(footerFile)) 216 | if err != nil { 217 | http.Error(w, "Error parsing footer template", http.StatusInternalServerError) 218 | return 219 | } 220 | 221 | // 直接将HTML内容发送给客户端 222 | w.Header().Set("Content-Type", "text/html") 223 | err = tmpl.Execute(w, nil) 224 | if err != nil { 225 | http.Error(w, "Error rendering HTML template", http.StatusInternalServerError) 226 | } 227 | } 228 | 229 | func Pwd(w http.ResponseWriter, r *http.Request) { 230 | // 输出 HTML 表单 231 | if r.Method != http.MethodPost { 232 | file, err := assets.Templates.ReadFile("templates/pwd.tmpl") 233 | if err != nil { 234 | http.Error(w, "HTML file not found", http.StatusNotFound) 235 | return 236 | } 237 | // 读取头部模板 238 | headerFile, err := assets.Templates.ReadFile("templates/header.tmpl") 239 | if err != nil { 240 | http.Error(w, "Header template not found", http.StatusNotFound) 241 | return 242 | } 243 | 244 | // 创建HTML模板并包括头部 245 | tmpl := template.New("html") 246 | if tmpl, err = tmpl.Parse(string(headerFile)); err != nil { 247 | http.Error(w, "Error parsing Header template", http.StatusInternalServerError) 248 | return 249 | } 250 | 251 | // 包括主HTML内容 252 | if tmpl, err = tmpl.Parse(string(file)); err != nil { 253 | http.Error(w, "Error parsing File template", http.StatusInternalServerError) 254 | return 255 | } 256 | 257 | // 直接将HTML内容发送给客户端 258 | w.Header().Set("Content-Type", "text/html") 259 | if err := tmpl.Execute(w, nil); err != nil { 260 | http.Error(w, "Error rendering HTML template", http.StatusInternalServerError) 261 | } 262 | return 263 | } 264 | // 设置cookie 265 | cookie := http.Cookie{ 266 | Name: "p", 267 | Value: r.FormValue("p"), 268 | } 269 | http.SetCookie(w, &cookie) 270 | http.Redirect(w, r, "/", http.StatusSeeOther) 271 | } 272 | 273 | func Middleware(next http.HandlerFunc) http.HandlerFunc { 274 | return func(w http.ResponseWriter, r *http.Request) { 275 | // 只有当密码设置并且不为"none"时,才进行检查 276 | if conf.Pass != "" && conf.Pass != "none" { 277 | if strings.HasPrefix(r.URL.Path, "/api") && r.URL.Query().Get("pass") == conf.Pass { 278 | return 279 | } 280 | if cookie, err := r.Cookie("p"); err != nil || cookie.Value != conf.Pass { 281 | http.Redirect(w, r, "/pwd", http.StatusSeeOther) 282 | return 283 | } 284 | } 285 | next(w, r) 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /get.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ARCH=$(uname -m) 4 | 5 | if [ "$ARCH" == "x86_64" ]; then 6 | File="tgState.zip" 7 | elif [ "$ARCH" == "aarch64" ]; then 8 | File="tgState_arm64.zip" 9 | else 10 | echo "Unsupported architecture: $ARCH" 11 | exit 1 12 | fi 13 | 14 | # Download and unzip 15 | wget "https://github.com/csznet/tgState/releases/latest/download/$File" && unzip "$File" && rm "$File" 16 | 17 | # Set permissions 18 | chmod +x tgState 19 | 20 | echo "successfully." 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module csz.net/tgstate 2 | 3 | go 1.20 4 | 5 | require github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= 2 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= 3 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "os" 9 | 10 | "csz.net/tgstate/conf" 11 | "csz.net/tgstate/control" 12 | "csz.net/tgstate/utils" 13 | ) 14 | 15 | var webPort string 16 | var OptApi = true 17 | 18 | func main() { 19 | //判断是否设置参数 20 | if conf.BotToken == "" || conf.ChannelName == "" { 21 | fmt.Println("请先设置Bot Token和对象") 22 | return 23 | } 24 | go utils.BotDo() 25 | web() 26 | } 27 | 28 | func web() { 29 | http.HandleFunc(conf.FileRoute, control.D) 30 | if OptApi { 31 | if conf.Pass != "" && conf.Pass != "none" { 32 | http.HandleFunc("/pwd", control.Pwd) 33 | } 34 | http.HandleFunc("/api", control.Middleware(control.UploadImageAPI)) 35 | http.HandleFunc("/", control.Middleware(control.Index)) 36 | } 37 | 38 | if listener, err := net.Listen("tcp", ":"+webPort); err != nil { 39 | fmt.Printf("端口 %s 已被占用\n", webPort) 40 | } else { 41 | defer listener.Close() 42 | fmt.Printf("启动Web服务器,监听端口 %s\n", webPort) 43 | if err := http.Serve(listener, nil); err != nil { 44 | fmt.Println(err) 45 | } 46 | } 47 | } 48 | 49 | func init() { 50 | flag.StringVar(&webPort, "port", "8088", "Web Port") 51 | flag.StringVar(&conf.BotToken, "token", os.Getenv("token"), "Bot Token") 52 | flag.StringVar(&conf.ChannelName, "target", os.Getenv("target"), "Channel Name or ID") 53 | flag.StringVar(&conf.Pass, "pass", os.Getenv("pass"), "Visit Password") 54 | flag.StringVar(&conf.Mode, "mode", os.Getenv("mode"), "Run mode") 55 | flag.StringVar(&conf.BaseUrl, "url", os.Getenv("url"), "Base Url") 56 | flag.Parse() 57 | if conf.Mode == "m" { 58 | OptApi = false 59 | } 60 | if conf.Mode != "p" && conf.Mode != "m" { 61 | conf.Mode = "p" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "strconv" 8 | "strings" 9 | 10 | "csz.net/tgstate/conf" 11 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 12 | ) 13 | 14 | func TgFileData(fileName string, fileData io.Reader) tgbotapi.FileReader { 15 | return tgbotapi.FileReader{ 16 | Name: fileName, 17 | Reader: fileData, 18 | } 19 | } 20 | 21 | func UpDocument(fileData tgbotapi.FileReader) string { 22 | bot, err := tgbotapi.NewBotAPI(conf.BotToken) 23 | if err != nil { 24 | log.Println(err) 25 | return "" 26 | } 27 | // Upload the file to Telegram 28 | params := tgbotapi.Params{ 29 | "chat_id": conf.ChannelName, // Replace with the chat ID where you want to send the file 30 | } 31 | files := []tgbotapi.RequestFile{ 32 | { 33 | Name: "document", 34 | Data: fileData, 35 | }, 36 | } 37 | response, err := bot.UploadFiles("sendDocument", params, files) 38 | if err != nil { 39 | log.Panic(err) 40 | } 41 | var msg tgbotapi.Message 42 | json.Unmarshal([]byte(response.Result), &msg) 43 | var resp string 44 | switch { 45 | case msg.Document != nil: 46 | resp = msg.Document.FileID 47 | case msg.Audio != nil: 48 | resp = msg.Audio.FileID 49 | case msg.Video != nil: 50 | resp = msg.Video.FileID 51 | case msg.Sticker != nil: 52 | resp = msg.Sticker.FileID 53 | } 54 | return resp 55 | } 56 | 57 | func GetDownloadUrl(fileID string) (string, bool) { 58 | bot, err := tgbotapi.NewBotAPI(conf.BotToken) 59 | if err != nil { 60 | log.Panic(err) 61 | } 62 | // 使用 getFile 方法获取文件信息 63 | file, err := bot.GetFile(tgbotapi.FileConfig{FileID: fileID}) 64 | if err != nil { 65 | log.Println("获取文件失败【" + fileID + "】") 66 | log.Println(err) 67 | return "", false 68 | } 69 | log.Println("获取文件成功【" + fileID + "】") 70 | // 获取文件下载链接 71 | fileURL := file.Link(conf.BotToken) 72 | return fileURL, true 73 | } 74 | func BotDo() { 75 | bot, err := tgbotapi.NewBotAPI(conf.BotToken) 76 | if err != nil { 77 | log.Println(err) 78 | return 79 | } 80 | u := tgbotapi.NewUpdate(0) 81 | u.Timeout = 60 82 | updatesChan := bot.GetUpdatesChan(u) 83 | for update := range updatesChan { 84 | var msg *tgbotapi.Message 85 | if update.Message != nil { 86 | msg = update.Message 87 | } 88 | if update.ChannelPost != nil { 89 | msg = update.ChannelPost 90 | } 91 | if msg != nil && msg.Text == "get" && msg.ReplyToMessage != nil { 92 | var fileID string 93 | switch { 94 | case msg.ReplyToMessage.Document != nil && msg.ReplyToMessage.Document.FileID != "": 95 | fileID = msg.ReplyToMessage.Document.FileID 96 | case msg.ReplyToMessage.Video != nil && msg.ReplyToMessage.Video.FileID != "": 97 | fileID = msg.ReplyToMessage.Video.FileID 98 | case msg.ReplyToMessage.Sticker != nil && msg.ReplyToMessage.Sticker.FileID != "": 99 | fileID = msg.ReplyToMessage.Sticker.FileID 100 | } 101 | if fileID != "" { 102 | newMsg := tgbotapi.NewMessage(msg.Chat.ID, strings.TrimSuffix(conf.BaseUrl, "/")+"/d/"+fileID) 103 | newMsg.ReplyToMessageID = msg.MessageID 104 | if !strings.HasPrefix(conf.ChannelName, "@") { 105 | if man, err := strconv.Atoi(conf.ChannelName); err == nil && int(msg.Chat.ID) == man { 106 | bot.Send(newMsg) 107 | } 108 | } else { 109 | bot.Send(newMsg) 110 | } 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "src": "/.*", 5 | "dest": "/api/vercel.go" 6 | } 7 | ] 8 | } --------------------------------------------------------------------------------