├── .idea ├── .gitignore ├── modules.xml ├── telegrambot.iml └── vcs.xml ├── Dockerfile ├── DockerfileDev ├── LICENSE ├── README.md ├── cmd └── main.go ├── conf.yaml ├── config └── config.go ├── go.mod ├── go.sum ├── internal ├── bot │ ├── bot.go │ ├── handlers │ │ ├── callback_query.go │ │ └── command.go │ └── keyboard │ │ └── key_board.go ├── db │ ├── db.go │ ├── models │ │ └── domain.go │ └── repository │ │ └── domain_repo.go ├── services │ ├── check.go │ ├── cloudflare.go │ └── version.go └── utils │ ├── listen.go │ ├── logger.go │ └── tool.go ├── photo.jpg └── tests └── services_test.go /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/telegrambot.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用官方的 Go 镜像作为基础镜像 2 | FROM golang:1.21.4-alpine AS builder 3 | 4 | # 安装必要的工具来下载和解压文件 5 | RUN apk add --no-cache curl unzip 6 | # 安装 SQLite 依赖库(为了支持 go-sqlite3 驱动) 7 | RUN apk add --no-cache sqlite sqlite-dev 8 | # 安装构建工具和 C 编译器 9 | RUN apk add --no-cache gcc musl-dev 10 | 11 | # 设置工作目录 12 | WORKDIR /app 13 | 14 | # 从 GitHub 下载仓库并解压 15 | RUN curl -L https://github.com/reppoor/telegram-bot-ddns/archive/refs/heads/master.zip -o telegram-bot-ddns-master.zip \ 16 | && unzip telegram-bot-ddns-master.zip \ 17 | && rm telegram-bot-ddns-master.zip 18 | 19 | # 重命名解压后的文件夹(相对路径) 20 | RUN mv telegram-bot-ddns-master telegrambot 21 | 22 | # 设置安装依赖的变量环境 23 | RUN go env -w GO111MODULE=on 24 | RUN go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct 25 | 26 | # 切换到解压后的文件夹,并安装 Go 依赖 27 | WORKDIR /app/telegrambot 28 | RUN go mod tidy 29 | # 设置 CGO_ENABLED 环境变量为 1 启用 CGO 30 | ENV CGO_ENABLED=1 31 | # 构建 Go 应用 32 | RUN go build -o cmd/main cmd/main.go 33 | # 使用轻量级的基础镜像来运行应用 34 | FROM alpine:latest 35 | # 创建工作目录 36 | WORKDIR /app 37 | 38 | # 从构建阶段复制构建产物 39 | COPY --from=builder /app/telegrambot/cmd/main /app/ 40 | # 从构建阶段复制构建产物 41 | COPY --from=builder /app/telegrambot/go.mod /app/ 42 | # 复制配置文件 43 | COPY --from=builder /app/telegrambot/conf.yaml /app/ 44 | 45 | 46 | # 设置容器启动命令 47 | CMD ["./main"] -------------------------------------------------------------------------------- /DockerfileDev: -------------------------------------------------------------------------------- 1 | # 使用官方的 Go 镜像作为基础镜像 2 | FROM golang:1.21.4-alpine AS builder 3 | 4 | # 安装必要的工具来下载和解压文件 5 | RUN apk add --no-cache curl unzip 6 | # 安装 SQLite 依赖库(为了支持 go-sqlite3 驱动) 7 | RUN apk add --no-cache sqlite sqlite-dev 8 | # 安装构建工具和 C 编译器 9 | RUN apk add --no-cache gcc musl-dev 10 | 11 | # 设置工作目录 12 | WORKDIR /app 13 | 14 | # 从 GitHub 下载仓库并解压 15 | RUN curl -L https://github.com/reppoor/telegram-bot-ddns/archive/refs/heads/dev.zip -o telegram-bot-ddns-master.zip \ 16 | && unzip telegram-bot-ddns-master.zip \ 17 | && rm telegram-bot-ddns-master.zip 18 | 19 | # 重命名解压后的文件夹(相对路径) 20 | RUN mv telegram-bot-ddns-dev telegrambot 21 | 22 | # 设置安装依赖的变量环境 23 | RUN go env -w GO111MODULE=on 24 | RUN go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct 25 | 26 | # 切换到解压后的文件夹,并安装 Go 依赖 27 | WORKDIR /app/telegrambot 28 | RUN go mod tidy 29 | 30 | # 设置 CGO_ENABLED 环境变量为 1 启用 CGO 31 | ENV CGO_ENABLED=1 32 | 33 | 34 | RUN ls /app/telegrambot 35 | # 构建 Go 应用 36 | RUN go build -o cmd/main cmd/main.go 37 | # 使用轻量级的基础镜像来运行应用 38 | FROM alpine:latest 39 | # 创建工作目录 40 | WORKDIR /app 41 | 42 | # 从构建阶段复制构建产物 43 | COPY --from=builder /app/telegrambot/cmd/main /app/ 44 | # 从构建阶段复制构建产物 45 | COPY --from=builder /app/telegrambot/go.mod /app/ 46 | # 复制配置文件 47 | COPY --from=builder /app/telegrambot/conf.yaml /app/ 48 | 49 | 50 | # 设置容器启动命令 51 | CMD ["./main"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # telegram-bot-DDNS 2 | [telegram-bot-DDNS](https://github.com/reppoor/telegram-bot-ddns) 3 | 4 | [telegram频道](https://t.me/ddns_reppoor) 5 | # 一款Telegram动态域名解析机器人 6 | 仅支持IPV4,IPV6请绕道 7 | 8 | 仅接受cloudflare托管的域名 9 | 10 | ![描述文本](photo.jpg) 11 | # 开发环境 12 | GO >= 1.21.4 13 | 14 | MYSQL > = 5.7.34 15 | 16 | # 功能特性 17 | 1.一键解析A记录到绑定域名 18 | 19 | 2.定时监控域名连通性进行自动切换 20 | 21 | 3.检测方法为TCP三次握手 22 | 23 | # 准备工作 24 | 25 | #### 如果检测对象为中国的服务器且屏蔽海外IP的机器,建议准备一台IP地区为中国的VPS 26 | 27 | VPS操作系统建议Debian/Ubuntu 28 | 29 | 30 | # 运行方式 31 | #### 1.安装aaPanel 32 | ``` 33 | URL=https://www.aapanel.com/script/install_7.0_en.sh && if [ -f /usr/bin/curl ];then curl -ksSO "$URL" ;else wget --no-check-certificate -O install_7.0_en.sh "$URL";fi;bash install_7.0_en.sh aapanel 34 | ``` 35 | #### 2.进入aaPanel安装docker(如果需要使用mysql数据库,您可能需要下载mysql,创建好数据库并记住好数据库的账号密码) 36 | 37 | #### 3.需要在宿主机root创建conf.yaml文件 38 | ``` 39 | database: 40 | type: "sqlite" # 数据库类型,可选mysql和sqlite,如果选mysql就需要填用户名和密码等配置 41 | file: "./database.db" #sqlite的文件路径,请不要更改路径 42 | user: "" # 数据库用户名 43 | password: "" # 数据库密码 44 | host: "" # 数据库主机 45 | port: "3306" # 数据库端口 46 | name: "" # 数据库名称 47 | charset: "utf8mb4" # 字符集 48 | 49 | telegram: 50 | id : #telegram用户ID 51 | token : "" # telegram机器人Token找@BotFather创建 52 | apiEndpoint: "https://api.telegram.org" #telegramAPI 可以反代,如果不知道在做什么,请不要更改 53 | 54 | cloudflare: 55 | email: "" #cloudflare的email 56 | key: "" #cloudflare的key 57 | 58 | network: 59 | enable_proxy: true # 开启:true,关闭:false。开启后一定要保证代理语法正确,否则程序报错 60 | proxy: "http://user:pass@127.0.0.1:7890" #配置telegram代理,支持http和socks5。示例语法 socks5://127.0.0.1:7890 账号和用户名示例语法socks5://user:pass@127.0.0.1:7890 61 | 62 | check: 63 | ip_check_time : 3 # 单位秒Second 64 | check_time: 10 #单位分钟Minute (建议超过5分钟,否则报错) 65 | ``` 66 | #### 4.在宿主主机的root目录下创建database.db文件,名字用这个即可,创建完毕后,同时赋予该文件所有权限 67 | 68 | (如果需要用sqlite数据库,否则忽略这条即可) 69 | ``` 70 | sudo chmod 777 /root/database.db 71 | ``` 72 | #### 5.下拉docker镜像并运行容器(使用mysql数据库的命令) 73 | ``` 74 | docker pull reppoor/telegram-bot-ddns:latest && docker run -d -v /root/conf.yaml:/app/conf.yaml reppoor/telegram-bot-ddns:latest 75 | ``` 76 | #### 6.下拉docker镜像并运行容器(使用sqlite数据库的命令) 77 | ``` 78 | docker pull reppoor/telegram-bot-ddns:latest && docker run -d -v /root/conf.yaml:/app/conf.yaml -v /root/database.db:/app/database.db reppoor/telegram-bot-ddns:latest 79 | ``` 80 | 81 | #### 5.启动后去容器查看日记,可以看到启动失败还是成功 82 | 83 | # 初始化机器人 84 | 85 | #### 找@BotFather,进入自己的机器人 86 | 87 | 1.点击Edit Bot 88 | 89 | 2.点击Edit Commands 90 | 91 | 3.输入如下命令发送 92 | ``` 93 | start - 开始 94 | version - 当前版本 95 | id - 获取ID 96 | init - bot初始化 97 | info - 转发信息 98 | insert - 插入转发记录 99 | check - 检测连通性 100 | parse - 获取当前解析状态 101 | getip - 批量获取转发ip 102 | ``` 103 | 在docker启动后首先点击该命令,否则无法使用 104 | ``` 105 | /init 进行初始化数据库,否则无法使用 106 | ``` 107 | # 转发运营商收录清单(持续更新中) 108 | 109 | ### [池雨转发:t.me/chiyuzf](https://t.me/ddns_reppoor) (开户要求:无。50/T) 110 | 111 | 112 | # Stargazers over time 113 | [![Stargazers over time](https://starchart.cc/reppoor/telegram-bot-ddns.svg?variant=adaptive)](https://starchart.cc/reppoor/telegram-bot-ddns) -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "telegrambot/internal/bot" 6 | "telegrambot/internal/services" 7 | ) 8 | 9 | func main() { 10 | v := services.Version() 11 | fmt.Print(v + "\n") 12 | bot.TelegramApp() // APP入口 13 | } 14 | -------------------------------------------------------------------------------- /conf.yaml: -------------------------------------------------------------------------------- 1 | database: 2 | type: "sqlite" # 数据库类型,可选mysql和sqlite,如果选mysql就需要填用户名和密码等配置 3 | file: "./database.db" #sqlite的文件路径,请不要更改路径 4 | user: "" # 数据库用户名 5 | password: "" # 数据库密码 6 | host: "" # 数据库主机 7 | port: "3306" # 数据库端口 8 | name: "" # 数据库名称 9 | charset: "utf8mb4" # 字符集 10 | 11 | telegram: 12 | id : #telegram用户ID 13 | token : "" # telegram机器人Token找@BotFather创建 14 | apiEndpoint: "https://api.telegram.org" #telegramAPI 可以反代,如果不知道在做什么,请不要更改 15 | 16 | cloudflare: 17 | email: "" #cloudflare的email 18 | key: "" #cloudflare的key 19 | 20 | network: 21 | enable_proxy: true # 开启:true,关闭:false。开启后一定要保证代理语法正确,否则程序报错。如果使用反代API,请关闭代理 22 | proxy: "socks5://user:pass@127.0.0.1:7890" #配置telegram代理,支持http和socks5。示例语法 socks5://127.0.0.1:7890 账号和用户名示例语法socks5://user:pass@127.0.0.1:7890 23 | 24 | check: 25 | ip_check_time : 3 # 单位秒Second 26 | check_time: 10 #单位分钟Minute (建议超过5分钟,否则报错) -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | // Config 配置加载逻辑 13 | type Config struct { 14 | Database struct { 15 | Type string `yaml:"type"` // 数据库用户名 16 | File string `yaml:"file"` // 数据库用户名 17 | User string `yaml:"user"` // 数据库用户名 18 | Password string `yaml:"password"` // 数据库密码 19 | Host string `yaml:"host"` // 数据库主机 20 | Port string `yaml:"port"` // 数据库端口 21 | Name string `yaml:"name"` // 数据库名称 22 | Charset string `yaml:"charset"` // 数据库字符集 23 | } `yaml:"database"` 24 | 25 | Cloudflare struct { 26 | Email string `yaml:"email"` // cloudflare email 27 | Key string `yaml:"key"` // cloudflare key 28 | } `yaml:"cloudflare"` 29 | 30 | Telegram struct { 31 | Id int64 `yaml:"id"` // telegram机器人ID 32 | Token string `yaml:"token"` // telegram机器人token 33 | ApiEndpoint string `yaml:"apiEndpoint"` // telegramAPI 34 | } `yaml:"telegram"` 35 | 36 | Network struct { 37 | EnableProxy bool `yaml:"enable_proxy"` // 是否启用telegram代理 38 | Proxy string `yaml:"proxy"` // telegram网络代理地址 39 | } `yaml:"network"` 40 | 41 | Check struct { 42 | IpCheckTime int `yaml:"ip_check_time"` // 每秒检测时间 43 | CheckTime int `yaml:"check_time"` // 每分钟检测时间 44 | } `yaml:"check"` 45 | } 46 | 47 | // LoadConfig 加载 YAML 配置文件 48 | func LoadConfig(filePath string) (*Config, error) { 49 | // 如果 filePath 为空,则使用默认相对路径 50 | if filePath == "1" { 51 | exePath, err := os.Executable() // 获取当前可执行文件路径 52 | if err != nil { 53 | log.Printf("无法获取可执行文件路径: %v", err) 54 | return nil, err 55 | } 56 | filePath = filepath.Join(filepath.Dir(exePath), "conf.yaml") // 默认文件名 57 | } 58 | workingDir, _ := os.Getwd() 59 | // 查找项目根目录 60 | rootDir, err := findProjectRoot(workingDir) 61 | if err != nil { 62 | fmt.Println("错误:", err) 63 | return nil, fmt.Errorf("错误:%s", err) 64 | } 65 | //filePath = filepath.Join(filepath.Dir(rootDir), "conf.yaml") // 默认文件名 66 | //fmt.Println(rootDir + "/conf.yaml") //打印conf.yaml路径情况,进行调试 67 | file, err := os.Open(rootDir + "/conf.yaml") 68 | if err != nil { 69 | log.Printf("无法打开配置文件 %s: %v", filePath, err) 70 | return nil, err 71 | } 72 | defer func(file *os.File) { 73 | err := file.Close() 74 | if err != nil { 75 | 76 | } 77 | }(file) 78 | 79 | var config Config 80 | decoder := yaml.NewDecoder(file) 81 | if err := decoder.Decode(&config); err != nil { 82 | log.Printf("解析配置文件 %s 失败: %v", filePath, err) 83 | return nil, err 84 | } 85 | 86 | return &config, nil 87 | } 88 | 89 | // 查找项目根目录 90 | func findProjectRoot(startDir string) (string, error) { 91 | // 假设根目录有一个标志文件,如 go.mod 或 README.md 92 | // 你可以根据项目的实际情况选择其他标志文件 93 | for { 94 | // 检查是否有 go.mod 文件(可以修改为其他标志文件) 95 | if _, err := os.Stat(filepath.Join(startDir, "go.mod")); err == nil { 96 | return startDir, nil 97 | } 98 | 99 | // 向上遍历父目录 100 | parentDir := filepath.Dir(startDir) 101 | if parentDir == startDir { // 如果已经到达根目录,停止查找 102 | break 103 | } 104 | startDir = parentDir 105 | fmt.Printf(startDir) 106 | } 107 | 108 | return "", fmt.Errorf("项目根目录未找到") 109 | 110 | } 111 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module telegrambot 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/cloudflare/cloudflare-go v0.111.0 7 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 8 | gopkg.in/yaml.v3 v3.0.1 9 | gorm.io/driver/mysql v1.5.7 10 | gorm.io/driver/sqlite v1.5.7 11 | gorm.io/gorm v1.25.12 12 | ) 13 | 14 | require ( 15 | github.com/go-sql-driver/mysql v1.7.0 // indirect 16 | github.com/goccy/go-json v0.10.3 // indirect 17 | github.com/google/go-querystring v1.1.0 // indirect 18 | github.com/jinzhu/inflection v1.0.0 // indirect 19 | github.com/jinzhu/now v1.1.5 // indirect 20 | github.com/kr/text v0.2.0 // indirect 21 | github.com/mattn/go-sqlite3 v1.14.22 // indirect 22 | golang.org/x/net v0.32.0 // indirect 23 | golang.org/x/text v0.21.0 // indirect 24 | golang.org/x/time v0.8.0 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cloudflare/cloudflare-go v0.111.0 h1:bFgl5OyR7iaV9DkTaoI2jU8X4rXDzEaFDaPfMTp+Ewo= 2 | github.com/cloudflare/cloudflare-go v0.111.0/go.mod h1:w5c4Vm00JjZM+W0mPi6QOC+eWLncGQPURtgDck3z5xU= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= 7 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 8 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= 9 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= 10 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 11 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 12 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 13 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 14 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 15 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 16 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 17 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 18 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 19 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 20 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 21 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 22 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 23 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 24 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 25 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 26 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 27 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= 30 | github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= 31 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 32 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 33 | golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= 34 | golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= 35 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 36 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 37 | golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 38 | golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 39 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 41 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 42 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 43 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 44 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= 46 | gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= 47 | gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= 48 | gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= 49 | gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 50 | gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= 51 | gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 52 | -------------------------------------------------------------------------------- /internal/bot/bot.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 7 | "golang.org/x/net/proxy" 8 | "log" 9 | "net" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | "telegrambot/config" 14 | "telegrambot/internal/bot/handlers" 15 | "telegrambot/internal/services" 16 | "time" 17 | ) 18 | 19 | func TelegramApp() { 20 | // 加载配置文件 21 | Config, err := config.LoadConfig("") 22 | if err != nil { 23 | log.Fatalf("加载配置文件失败: %v", err) 24 | } 25 | var bot *tgbotapi.BotAPI 26 | // 假设 Config.Network.Proxy 是代理地址,Config.Network.EnableProxy 是是否启用代理 27 | if Config.Network.EnableProxy { 28 | var httpClient *http.Client 29 | proxyURL, err := url.Parse(Config.Network.Proxy) 30 | if err != nil { 31 | log.Fatalf("解析代理地址失败: %v", err) 32 | } 33 | 34 | // 提取代理用户名和密码 35 | var proxyAuth *url.Userinfo 36 | if proxyURL.User != nil { 37 | proxyAuth = proxyURL.User 38 | } 39 | 40 | // 判断代理类型,HTTP 或 SOCKS5 41 | if strings.HasPrefix(proxyURL.Scheme, "http") { 42 | fmt.Println("使用http代理建立telegram连接") 43 | // 如果是 HTTP 代理 44 | transport := &http.Transport{ 45 | Proxy: func(req *http.Request) (*url.URL, error) { 46 | // 获取用户名和密码 47 | username := proxyAuth.Username() 48 | password, _ := proxyAuth.Password() 49 | 50 | proxyURLWithAuth := &url.URL{ 51 | Scheme: "http", 52 | Host: proxyURL.Host, 53 | User: url.UserPassword(username, password), 54 | } 55 | return proxyURLWithAuth, nil 56 | }, 57 | DialContext: (&net.Dialer{ 58 | Timeout: 10 * time.Second, // 连接超时 59 | }).DialContext, 60 | ResponseHeaderTimeout: 10 * time.Second, // 读取响应头的超时 61 | } 62 | 63 | // 设置 httpClient 的超时 64 | httpClient = &http.Client{ 65 | Timeout: 30 * time.Second, // 总超时(连接 + 读取 + 写入) 66 | Transport: transport, 67 | } 68 | } else if strings.HasPrefix(proxyURL.Scheme, "socks5") { 69 | fmt.Println("使用socks5代理建立telegram连接") 70 | // 如果是 SOCKS5 代理 71 | var dialer proxy.Dialer 72 | if proxyAuth != nil { 73 | // 如果 SOCKS5 代理需要认证 74 | username := proxyAuth.Username() 75 | password, _ := proxyAuth.Password() // 只取密码部分 76 | 77 | // 创建带认证的 SOCKS5 代理 78 | dialer, err = proxy.SOCKS5("tcp", proxyURL.Host, &proxy.Auth{ 79 | User: username, 80 | Password: password, 81 | }, proxy.Direct) 82 | if err != nil { 83 | log.Fatalf("连接到 SOCKS5 代理失败: %v", err) 84 | } 85 | } else { 86 | // 如果 SOCKS5 代理不需要认证 87 | dialer, err = proxy.SOCKS5("tcp", proxyURL.Host, nil, proxy.Direct) 88 | if err != nil { 89 | log.Fatalf("连接到 SOCKS5 代理失败: %v", err) 90 | } 91 | } 92 | 93 | // 包装 dialer.Dial 成一个支持 DialContext 的方法 94 | httpClient = &http.Client{ 95 | Transport: &http.Transport{ 96 | DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { 97 | // 使用代理的 Dial 方法 98 | return dialer.Dial(network, address) 99 | }, 100 | }, 101 | } 102 | 103 | } else { 104 | log.Fatalf("不支持的代理类型: %s", proxyURL.Scheme) 105 | } 106 | 107 | // 使用带代理的 httpClient 创建 Telegram Bot 108 | bot, err = tgbotapi.NewBotAPIWithClient(Config.Telegram.Token, Config.Telegram.ApiEndpoint+"/bot%s/%s", httpClient) 109 | if err != nil { 110 | log.Panic(err) 111 | } 112 | } else { 113 | bot, err = tgbotapi.NewBotAPIWithAPIEndpoint(Config.Telegram.Token, Config.Telegram.ApiEndpoint+"/bot%s/%s") 114 | if err != nil { 115 | log.Panic(err) 116 | } 117 | } 118 | u := tgbotapi.NewUpdate(0) 119 | u.Timeout = 60 // 设置超时时间 120 | updates := bot.GetUpdatesChan(u) // 获取更新通道 121 | // 在这里可以安全使用 bot 变量 122 | log.Printf("已授权账户: %s", bot.Self.UserName) 123 | // 创建一个单独的 Goroutine 用于定时任务 124 | go func() { 125 | 126 | ticker := time.NewTicker(time.Duration(Config.Check.CheckTime) * time.Minute) 127 | defer ticker.Stop() // 确保程序退出时停止Ticker 128 | for { 129 | select { 130 | case <-ticker.C: 131 | // 创建一个模拟的 Update 对象 132 | up := tgbotapi.Update{ 133 | Message: &tgbotapi.Message{ 134 | Chat: &tgbotapi.Chat{ 135 | ID: Config.Telegram.Id, // 指定目标 Chat ID 136 | }, 137 | }, 138 | } 139 | fmt.Println("定时检测任务启动") 140 | services.ALLCheckTCPConnectivity(bot, up, false) 141 | } 142 | } 143 | }() 144 | 145 | //轮询消息 146 | for update := range updates { 147 | // 异步处理回调查询 148 | if update.CallbackQuery != nil { 149 | go handlers.CallbackQuery(bot, update, Config) 150 | continue 151 | } 152 | // 仅处理包含消息的更新 153 | if update.Message != nil { 154 | go handlers.HandleCommand(bot, update, Config) 155 | continue 156 | } 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /internal/bot/handlers/callback_query.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 6 | "strings" 7 | "telegrambot/config" 8 | "telegrambot/internal/bot/keyboard" 9 | "telegrambot/internal/db" 10 | "telegrambot/internal/db/repository" 11 | "telegrambot/internal/services" 12 | ) 13 | 14 | func CallbackQuery(bot *tgbotapi.BotAPI, update tgbotapi.Update, Config *config.Config) { 15 | 16 | data := update.CallbackQuery.Data 17 | // 将回调数据按 '-' 分隔,判断菜单层级 18 | levels := strings.Split(data, "-") 19 | 20 | switch len(levels) { 21 | case 1: 22 | db.InitDB() //连接数据库 23 | DomainInfo, err := repository.GetDomainIDInfo(data) 24 | if err != nil { 25 | fmt.Println(err) 26 | return 27 | } 28 | ID := DomainInfo.ID 29 | Domain := DomainInfo.Domain 30 | ForwardingDomain := DomainInfo.ForwardingDomain 31 | IP := DomainInfo.IP 32 | Port := DomainInfo.Port 33 | ISP := DomainInfo.ISP 34 | Ban := DomainInfo.Ban 35 | // 格式化消息内容,使用 Markdown 格式 36 | messageText := fmt.Sprintf( 37 | "ID: `%d`\n域名: `%s`\n转发域名: `%s`\nIP: `%s`\n端口: `%d`\n运营商: `%s`\nIsBan: `%t`", 38 | ID, Domain, ForwardingDomain, IP, Port, ISP, Ban, 39 | ) // 格式化消息内容,使用 Markdown 格式 40 | fmt.Println(messageText) 41 | msg := tgbotapi.NewEditMessageText( 42 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 43 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 44 | messageText, // 新的消息文本 45 | ) 46 | msg.ParseMode = "Markdown" 47 | // 创建按钮 48 | msg.ReplyMarkup = keyboard.GenerateSubMenuKeyboard(ID, Ban) 49 | _, err = bot.Send(msg) 50 | fmt.Println("当前是1级菜单") 51 | case 2: 52 | if len(levels) > 1 { 53 | ID := levels[0] 54 | action := levels[1] 55 | 56 | switch action { 57 | case "del": 58 | // 处理删除操作 59 | fmt.Println("执行删除操作, ID:", ID) 60 | messageText := fmt.Sprintf("`正在删除该条记录...`") // 格式化消息内容,使用 Markdown 格式 61 | msg := tgbotapi.NewEditMessageText( 62 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 63 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 64 | messageText, // 新的消息文本 65 | ) 66 | msg.ParseMode = "Markdown" 67 | _, _ = bot.Send(msg) 68 | _, err := repository.DeleteDomainByID(data) 69 | if err != nil { 70 | messageText = fmt.Sprintf("`删除失败❌️`") // 格式化消息内容,使用 Markdown 格式 71 | msg = tgbotapi.NewEditMessageText( 72 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 73 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 74 | messageText, // 新的消息文本 75 | ) 76 | msg.ParseMode = "Markdown" 77 | _, _ = bot.Send(msg) 78 | return 79 | } 80 | db.InitDB() 81 | DomainInfo, err := repository.GetDomainInfo() 82 | if err != nil { 83 | fmt.Println(err) 84 | msg = tgbotapi.NewEditMessageText( 85 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 86 | update.CallbackQuery.Message.MessageID, 87 | "数据库未查询到任何域名记录❌️") // 要编辑的消息的 ID 88 | // 发送消息 89 | _, err = bot.Send(msg) 90 | return 91 | } 92 | keyBoard := keyboard.GenerateMainMenuKeyboard(DomainInfo) //生成内联键盘 93 | msg = tgbotapi.NewEditMessageText( 94 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 95 | update.CallbackQuery.Message.MessageID, 96 | "记录删除成功✅️") // 要编辑的消息的 ID 97 | msg.ReplyMarkup = &keyBoard 98 | // 发送消息 99 | _, err = bot.Send(msg) 100 | case "getIp": 101 | fmt.Println("获取转发最新ip轮询,正在开发中....") 102 | // 格式化消息内容,使用 Markdown 格式 103 | messageText := fmt.Sprintf("`正在获取最新IP...`") // 格式化消息内容,使用 Markdown 格式 104 | msg := tgbotapi.NewEditMessageText( 105 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 106 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 107 | messageText, // 新的消息文本 108 | ) 109 | msg.ParseMode = "Markdown" 110 | _, _ = bot.Send(msg) 111 | DomainInfo, err := repository.GetDomainIDInfo(data) 112 | if err != nil { 113 | fmt.Println("查询数据库失败", err) 114 | // 格式化消息内容,使用 Markdown 格式 115 | messageText = fmt.Sprintf("`查询数据库失败`") // 格式化消息内容,使用 Markdown 格式 116 | msg = tgbotapi.NewEditMessageText( 117 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 118 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 119 | messageText, // 新的消息文本 120 | ) 121 | msg.ParseMode = "Markdown" 122 | _, _ = bot.Send(msg) 123 | return 124 | } 125 | newIP, err := services.ResolveDomainToIP(DomainInfo.ForwardingDomain) //获取转发IP 126 | if err != nil { 127 | fmt.Println("获取IP失败", err) 128 | // 格式化消息内容,使用 Markdown 格式 129 | messageText = fmt.Sprintf("`获取IP失败`") // 格式化消息内容,使用 Markdown 格式 130 | msg = tgbotapi.NewEditMessageText( 131 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 132 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 133 | messageText, // 新的消息文本 134 | ) 135 | msg.ParseMode = "Markdown" 136 | _, _ = bot.Send(msg) 137 | return 138 | } 139 | newDomainIp, err := repository.UpdateDomainIp(data, newIP) 140 | if err != nil { 141 | fmt.Println("更新数据库IP失败", err) 142 | return 143 | } 144 | ID := newDomainIp.ID 145 | Domain := newDomainIp.Domain 146 | ForwardingDomain := newDomainIp.ForwardingDomain 147 | IP := newDomainIp.IP 148 | Port := newDomainIp.Port 149 | ISP := newDomainIp.ISP 150 | Ban := newDomainIp.Ban 151 | // 格式化消息内容,使用 Markdown 格式 152 | messageText = fmt.Sprintf( 153 | "*获取最新IP成功*✅\nID: `%d`\n域名: `%s`\n转发域名: `%s`\nIP: `%s`\n端口: `%d`\n运营商: `%s`\nIsBan: `%t`", 154 | ID, Domain, ForwardingDomain, IP, Port, ISP, Ban, 155 | ) // 格式化消息内容,使用 Markdown 格式 156 | fmt.Println(messageText) 157 | msg = tgbotapi.NewEditMessageText( 158 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 159 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 160 | messageText, // 新的消息文本 161 | ) 162 | msg.ParseMode = "Markdown" 163 | // 创建按钮 164 | msg.ReplyMarkup = keyboard.GenerateSubMenuKeyboard(ID, Ban) 165 | _, err = bot.Send(msg) 166 | case "parse": 167 | // 格式化消息内容,使用 Markdown 格式 168 | messageText := fmt.Sprintf("`正在解析DNS记录...`") // 格式化消息内容,使用 Markdown 格式 169 | msg := tgbotapi.NewEditMessageText( 170 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 171 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 172 | messageText, // 新的消息文本 173 | ) 174 | msg.ParseMode = "Markdown" 175 | _, _ = bot.Send(msg) 176 | // 处理解析操作 177 | fmt.Println("执行解析操作, ID:", ID) 178 | db.InitDB() //连接数据库 179 | DomainInfo, err := repository.GetDomainIDInfo(data) 180 | if err != nil { 181 | fmt.Println("查询数据库失败", err) 182 | // 格式化消息内容,使用 Markdown 格式 183 | messageText = fmt.Sprintf("`查询数据库失败`") // 格式化消息内容,使用 Markdown 格式 184 | msg = tgbotapi.NewEditMessageText( 185 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 186 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 187 | messageText, // 新的消息文本 188 | ) 189 | msg.ParseMode = "Markdown" 190 | _, _ = bot.Send(msg) 191 | return 192 | } 193 | newIP, err := services.ResolveDomainToIP(DomainInfo.ForwardingDomain) //获取转发IP 194 | if err != nil { 195 | fmt.Println("获取IP失败", err) 196 | // 格式化消息内容,使用 Markdown 格式 197 | messageText = fmt.Sprintf("`获取IP失败`") // 格式化消息内容,使用 Markdown 格式 198 | msg = tgbotapi.NewEditMessageText( 199 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 200 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 201 | messageText, // 新的消息文本 202 | ) 203 | msg.ParseMode = "Markdown" 204 | _, _ = bot.Send(msg) 205 | return 206 | } 207 | _, err = services.UpdateARecord(DomainInfo.Domain, newIP) 208 | if err != nil { 209 | fmt.Println("更新域名A记录失败", err) 210 | // 格式化消息内容,使用 Markdown 格式 211 | messageText = fmt.Sprintf("`更新域名A记录失败`") // 格式化消息内容,使用 Markdown 格式 212 | msg = tgbotapi.NewEditMessageText( 213 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 214 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 215 | messageText, // 新的消息文本 216 | ) 217 | msg.ParseMode = "Markdown" 218 | _, _ = bot.Send(msg) 219 | return 220 | } 221 | newDomainIp, err := repository.UpdateDomainIp(data, newIP) 222 | if err != nil { 223 | fmt.Println("更新数据库IP失败", err) 224 | return 225 | } 226 | ID := newDomainIp.ID 227 | Domain := newDomainIp.Domain 228 | ForwardingDomain := newDomainIp.ForwardingDomain 229 | IP := newDomainIp.IP 230 | Port := newDomainIp.Port 231 | ISP := newDomainIp.ISP 232 | Ban := newDomainIp.Ban 233 | // 格式化消息内容,使用 Markdown 格式 234 | messageText = fmt.Sprintf( 235 | "*解析成功*✅\nID: `%d`\n域名: `%s`\n转发域名: `%s`\nIP: `%s`\n端口: `%d`\n运营商: `%s`\nIsBan: `%t`", 236 | ID, Domain, ForwardingDomain, IP, Port, ISP, Ban, 237 | ) // 格式化消息内容,使用 Markdown 格式 238 | fmt.Println(messageText) 239 | msg = tgbotapi.NewEditMessageText( 240 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 241 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 242 | messageText, // 新的消息文本 243 | ) 244 | msg.ParseMode = "Markdown" 245 | // 创建按钮 246 | msg.ReplyMarkup = keyboard.GenerateSubMenuKeyboard(ID, Ban) 247 | _, err = bot.Send(msg) 248 | case "checkAndParse": 249 | // 检测连通性并解析记录 250 | fmt.Println("执行检测连通性并解析记录, ID:", ID) 251 | // 格式化消息内容,使用 Markdown 格式 252 | messageText := fmt.Sprintf("`正在检测连通性...`") // 格式化消息内容,使用 Markdown 格式 253 | msg := tgbotapi.NewEditMessageText( 254 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 255 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 256 | messageText, // 新的消息文本 257 | ) 258 | msg.ParseMode = "Markdown" 259 | _, _ = bot.Send(msg) 260 | db.InitDB() //连接数据库 261 | DomainInfo, err := repository.GetDomainIDInfo(data) 262 | if err != nil { 263 | fmt.Println("查询数据库失败", err) 264 | // 格式化消息内容,使用 Markdown 格式 265 | messageText = fmt.Sprintf("`查询数据库失败`") // 格式化消息内容,使用 Markdown 格式 266 | msg = tgbotapi.NewEditMessageText( 267 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 268 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 269 | messageText, // 新的消息文本 270 | ) 271 | msg.ParseMode = "Markdown" 272 | _, _ = bot.Send(msg) 273 | return 274 | } 275 | newIP, err := services.ResolveDomainToIP(DomainInfo.ForwardingDomain) //获取IP 276 | if err != nil { 277 | fmt.Println("获取IP失败", err) 278 | // 格式化消息内容,使用 Markdown 格式 279 | messageText = fmt.Sprintf("`获取IP失败`") // 格式化消息内容,使用 Markdown 格式 280 | msg = tgbotapi.NewEditMessageText( 281 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 282 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 283 | messageText, // 新的消息文本 284 | ) 285 | msg.ParseMode = "Markdown" 286 | _, _ = bot.Send(msg) 287 | return 288 | } 289 | if !services.CheckTCPConnectivity(newIP, DomainInfo.Port) { 290 | fmt.Println("节点异常", err) 291 | // 格式化消息内容,使用 Markdown 格式 292 | messageText = fmt.Sprintf("`节点异常`") // 格式化消息内容,使用 Markdown 格式 293 | msg = tgbotapi.NewEditMessageText( 294 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 295 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 296 | messageText, // 新的消息文本 297 | ) 298 | msg.ParseMode = "Markdown" 299 | _, _ = bot.Send(msg) 300 | return 301 | } 302 | // 格式化消息内容,使用 Markdown 格式 303 | messageText = fmt.Sprintf("`节点连通性正常,正在进行A记录解析...`") // 格式化消息内容,使用 Markdown 格式 304 | msg = tgbotapi.NewEditMessageText( 305 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 306 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 307 | messageText, // 新的消息文本 308 | ) 309 | msg.ParseMode = "Markdown" 310 | _, _ = bot.Send(msg) 311 | _, err = services.UpdateARecord(DomainInfo.Domain, newIP) 312 | if err != nil { 313 | fmt.Println("更新域名A记录失败", err) 314 | // 格式化消息内容,使用 Markdown 格式 315 | messageText = fmt.Sprintf("`更新域名A记录失败`") // 格式化消息内容,使用 Markdown 格式 316 | msg = tgbotapi.NewEditMessageText( 317 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 318 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 319 | messageText, // 新的消息文本 320 | ) 321 | msg.ParseMode = "Markdown" 322 | _, _ = bot.Send(msg) 323 | return 324 | } 325 | newDomainIp, err := repository.UpdateDomainIp(data, newIP) 326 | if err != nil { 327 | fmt.Println("更新数据库IP失败", err) 328 | return 329 | } 330 | ID := newDomainIp.ID 331 | Domain := newDomainIp.Domain 332 | ForwardingDomain := newDomainIp.ForwardingDomain 333 | IP := newDomainIp.IP 334 | Port := newDomainIp.Port 335 | ISP := newDomainIp.ISP 336 | Ban := newDomainIp.Ban 337 | // 格式化消息内容,使用 Markdown 格式 338 | messageText = fmt.Sprintf( 339 | "*检测并解析成功*✅️\nID: `%d`\n域名: `%s`\n转发域名: `%s`\nIP: `%s`\n端口: `%d`\n运营商: `%s`\nIsBan: `%t`", 340 | ID, Domain, ForwardingDomain, IP, Port, ISP, Ban, 341 | ) // 格式化消息内容,使用 Markdown 格式 342 | fmt.Println(messageText) 343 | msg = tgbotapi.NewEditMessageText( 344 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 345 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 346 | messageText, // 新的消息文本 347 | ) 348 | msg.ParseMode = "Markdown" 349 | // 创建键盘布局 350 | msg.ReplyMarkup = keyboard.GenerateSubMenuKeyboard(ID, Ban) 351 | //发送消息 352 | _, err = bot.Send(msg) 353 | case "ban": 354 | // 处理封禁操作 355 | fmt.Println("执行封禁或启用操作, ID:", data) 356 | db.InitDB() //连接数据库 357 | DomainInfo, err := repository.GetDomainIDInfo(data) 358 | if err != nil { 359 | fmt.Println(err) 360 | return 361 | } 362 | Ban := DomainInfo.Ban 363 | if Ban { 364 | newBanStatus := !DomainInfo.Ban 365 | _, err := repository.UpdateDomainBan(data, newBanStatus) 366 | if err != nil { 367 | fmt.Println(err) 368 | return 369 | } 370 | DomainInfo, err := repository.GetDomainIDInfo(data) 371 | if err != nil { 372 | fmt.Println(err) 373 | return 374 | } 375 | ID := DomainInfo.ID 376 | Domain := DomainInfo.Domain 377 | ForwardingDomain := DomainInfo.ForwardingDomain 378 | IP := DomainInfo.IP 379 | Port := DomainInfo.Port 380 | ISP := DomainInfo.ISP 381 | Ban := DomainInfo.Ban 382 | // 格式化消息内容,使用 Markdown 格式 383 | messageText := fmt.Sprintf( 384 | "*已解除封禁✅️*\nID: `%d`\n域名: `%s`\n转发域名: `%s`\nIP: `%s`\n端口: `%d`\n运营商: `%s`\nIsBan: `%t`", 385 | ID, Domain, ForwardingDomain, IP, Port, ISP, Ban, 386 | ) // 格式化消息内容,使用 Markdown 格式 387 | fmt.Println(messageText) 388 | msg := tgbotapi.NewEditMessageText( 389 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 390 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 391 | messageText, // 新的消息文本 392 | ) 393 | msg.ParseMode = "Markdown" 394 | // 创建按钮 395 | msg.ReplyMarkup = keyboard.GenerateSubMenuKeyboard(ID, Ban) 396 | _, err = bot.Send(msg) 397 | } else { 398 | newBanStatus := !DomainInfo.Ban 399 | _, err := repository.UpdateDomainBan(data, newBanStatus) 400 | if err != nil { 401 | fmt.Println(err) 402 | return 403 | } 404 | DomainInfo, err := repository.GetDomainIDInfo(data) 405 | if err != nil { 406 | fmt.Println(err) 407 | return 408 | } 409 | ID := DomainInfo.ID 410 | Domain := DomainInfo.Domain 411 | ForwardingDomain := DomainInfo.ForwardingDomain 412 | IP := DomainInfo.IP 413 | Port := DomainInfo.Port 414 | ISP := DomainInfo.ISP 415 | Ban := DomainInfo.Ban 416 | // 格式化消息内容,使用 Markdown 格式 417 | messageText := fmt.Sprintf( 418 | "*已封禁🚫*\nID: `%d`\n域名: `%s`\n转发域名: `%s`\nIP: `%s`\n端口: `%d`\n运营商: `%s`\nIsBan: `%t`", 419 | ID, Domain, ForwardingDomain, IP, Port, ISP, Ban, 420 | ) // 格式化消息内容,使用 Markdown 格式 421 | fmt.Println(messageText) 422 | msg := tgbotapi.NewEditMessageText( 423 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 424 | update.CallbackQuery.Message.MessageID, // 要编辑的消息的 ID 425 | messageText, // 新的消息文本 426 | ) 427 | msg.ParseMode = "Markdown" 428 | // 创建按钮 429 | msg.ReplyMarkup = keyboard.GenerateSubMenuKeyboard(ID, Ban) 430 | _, err = bot.Send(msg) 431 | } 432 | case "back": 433 | // 处理退出操作 434 | fmt.Println("返回操作, ID:", ID) 435 | db.InitDB() 436 | DomainInfo, err := repository.GetDomainInfo() 437 | if err != nil { 438 | fmt.Println(err) 439 | msg := tgbotapi.NewEditMessageText( 440 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 441 | update.CallbackQuery.Message.MessageID, 442 | "数据库未查询到任何域名记录❌️") // 要编辑的消息的 ID 443 | // 发送消息 444 | _, err = bot.Send(msg) 445 | return 446 | } 447 | keyBoard := keyboard.GenerateMainMenuKeyboard(DomainInfo) //生成内联键盘 448 | msg := tgbotapi.NewEditMessageText( 449 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 450 | update.CallbackQuery.Message.MessageID, 451 | "查询转发信息") // 要编辑的消息的 ID 452 | msg.ReplyMarkup = &keyBoard 453 | // 发送消息 454 | _, err = bot.Send(msg) 455 | case "exit": 456 | // 处理退出操作 457 | fmt.Println("退出操作, ID:", ID) 458 | // 删除消息 459 | msg := tgbotapi.NewDeleteMessage( 460 | update.CallbackQuery.Message.Chat.ID, // 原始消息的聊天 ID 461 | update.CallbackQuery.Message.MessageID, // 要删除的消息的 ID 462 | ) 463 | // 发送删除消息的请求 464 | _, _ = bot.Send(msg) 465 | 466 | } 467 | } 468 | 469 | fmt.Println("当前是2级菜单") 470 | case 3: 471 | fmt.Println("当前是3级菜单") 472 | default: 473 | msg := tgbotapi.NewMessage(update.CallbackQuery.Message.Chat.ID, "无效的回调数据") 474 | _, _ = bot.Send(msg) 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /internal/bot/handlers/command.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 6 | "log" 7 | "strconv" 8 | "strings" 9 | "telegrambot/config" 10 | "telegrambot/internal/bot/keyboard" 11 | "telegrambot/internal/db" 12 | "telegrambot/internal/db/repository" 13 | "telegrambot/internal/services" 14 | "telegrambot/internal/utils" 15 | ) 16 | 17 | // HandleCommand handleCommand 用于处理不同的命令 18 | func HandleCommand(bot *tgbotapi.BotAPI, update tgbotapi.Update, Config *config.Config) { 19 | 20 | ID := update.Message.From.ID //消息发送者ID 21 | FirstName := update.Message.From.FirstName //消息发送者名字 22 | LastName := update.Message.From.LastName //消息发送者姓氏 23 | UserName := update.Message.From.UserName //消息发送者用户名 24 | LanguageCode := update.Message.From.LanguageCode //消息发送者语言设置 25 | if update.Message.IsCommand() { 26 | switch update.Message.Command() { 27 | case "start": 28 | fmt.Printf("start命令\n") 29 | messageText := fmt.Sprintf("您好,很高兴为您服务") // 格式化消息内容,使用 Markdown 格式 30 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 31 | msg.ParseMode = "Markdown" 32 | _, _ = bot.Send(msg) 33 | return 34 | case "id": 35 | fmt.Printf("id命令\n") 36 | // 格式化消息内容,使用 Markdown 格式 37 | messageText := fmt.Sprintf("用户ID: `%d`\n名字: `%s`\n姓氏: `%s`\n用户名: [%s](https://t.me/%s)\n语言设置: `%s`", ID, FirstName, LastName, UserName, UserName, LanguageCode) // 格式化消息内容,使用 Markdown 格式 38 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 39 | msg.ParseMode = "Markdown" 40 | _, _ = bot.Send(msg) 41 | return 42 | case "init": 43 | fmt.Printf("init命令\n") 44 | if ID != Config.Telegram.Id { 45 | messageText := fmt.Sprintf("`您无法使用init命令`") // 格式化消息内容,使用 Markdown 格式 46 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 47 | msg.ParseMode = "Markdown" 48 | _, _ = bot.Send(msg) 49 | return 50 | } 51 | messageText := fmt.Sprintf("`机器人正常初始化数据库...`") // 格式化消息内容,使用 Markdown 格式 52 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 53 | msg.ParseMode = "Markdown" 54 | // 保存机器人发送的消息返回结果 55 | sentMsg, err := bot.Send(msg) 56 | if err != nil { 57 | fmt.Printf("发送初始化消息失败: %v\n", err) 58 | return 59 | } 60 | db.ATInitDB() 61 | db.CloseDB() 62 | // 编辑消息内容 63 | messageText = "`机器人数据库正常初始化完成`" // 格式化消息内容,使用 Markdown 格式 64 | editMsg := tgbotapi.NewEditMessageText( 65 | sentMsg.Chat.ID, // 聊天 ID 66 | sentMsg.MessageID, // 需要编辑的消息 ID 67 | messageText, // 新的消息内容 68 | ) 69 | editMsg.ParseMode = "Markdown" 70 | // 编辑消息 71 | _, _ = bot.Send(editMsg) 72 | return 73 | case "info": 74 | fmt.Printf("info命令\n") 75 | if ID != Config.Telegram.Id { 76 | messageText := fmt.Sprintf("`您无法使用info命令`") // 格式化消息内容,使用 Markdown 格式 77 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 78 | msg.ParseMode = "Markdown" 79 | _, _ = bot.Send(msg) 80 | return 81 | } 82 | db.InitDB() 83 | DomainInfo, err := repository.GetDomainInfo() 84 | if err != nil { 85 | fmt.Println(err) 86 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, 87 | "数据库未查询到任何域名记录❌️") // 要编辑的消息的 ID 88 | // 发送消息 89 | _, err = bot.Send(msg) 90 | return 91 | } 92 | keyBoard := keyboard.GenerateMainMenuKeyboard(DomainInfo) //生成内联键盘 93 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, "查询转发信息") 94 | msg.ReplyMarkup = keyBoard 95 | // 发送消息 96 | _, err = bot.Send(msg) 97 | return 98 | case "check": 99 | fmt.Printf("check命令\n") 100 | if ID != Config.Telegram.Id { 101 | messageText := fmt.Sprintf("`您无法使用check命令`") // 格式化消息内容,使用 Markdown 格式 102 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 103 | msg.ParseMode = "Markdown" 104 | _, _ = bot.Send(msg) 105 | return 106 | } 107 | services.ALLCheckTCPConnectivity(bot, update, true) 108 | return 109 | case "insert": 110 | fmt.Printf("insert命令\n") 111 | if ID != Config.Telegram.Id { 112 | messageText := fmt.Sprintf("`您无法使用insert命令`") // 格式化消息内容,使用 Markdown 格式 113 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 114 | msg.ParseMode = "Markdown" 115 | _, _ = bot.Send(msg) 116 | return 117 | } 118 | 119 | // 获取命令部分(例如 /insert) 120 | command := update.Message.Command() 121 | // 提取命令后面的部分(参数) 122 | params := strings.TrimSpace(update.Message.Text[len(command)+1:]) // 去掉 "/insert " 部分 123 | 124 | // 参数格式验证 125 | _, err := utils.ValidateFormat(params) 126 | if err != nil { 127 | messageText := fmt.Sprintf("*请参考格式:*\n"+ 128 | "*格式说明:*\n"+ 129 | "`主域名#转发域名#转发端口#运营商`\n"+ 130 | "*单条记录格式:*\n"+ 131 | "`www.baidu.com#www.hao123.com#7890#运营商`\n"+ 132 | "*批量记录格式转发域名用`|`分隔:*\n"+ 133 | "`www.baidu.com#www.hao123.com|www.4399.com#7890#运营商A|运营商B`\n"+ 134 | "*非法格式详情:*\n"+ 135 | "`%s`", err) // 格式化消息内容,使用 Markdown 格式 136 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 137 | msg.ParseMode = "Markdown" 138 | _, _ = bot.Send(msg) 139 | fmt.Println(err) 140 | return 141 | } 142 | // 解析参数 143 | fmt.Printf(params + "\n") 144 | parts := strings.Split(params, "#") 145 | // 获取主要域名和需要遍历的域名列表 146 | primaryDomain := strings.TrimSpace(parts[0]) // 主要域名 147 | domainList := strings.Split(parts[1], "|") // 遍历的域名 148 | port, err := strconv.Atoi(parts[2]) // 端口号 149 | if err != nil { 150 | messageText := "*端口号格式错误,请输入数字*" 151 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 152 | msg.ParseMode = "Markdown" 153 | _, _ = bot.Send(msg) 154 | return 155 | } 156 | 157 | // 处理运营商字段 158 | operatorList := strings.Split(parts[3], "|") 159 | 160 | // 检查域名和运营商是否一一对应 161 | if len(domainList) != len(operatorList) { 162 | messageText := "*格式错误:* `域名列表和运营商列表数量不匹配,请检查`\n例如: \n`www.baidu.com#www.hao123.com|www.4399.com#7890#运营商A|运营商B`" 163 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 164 | msg.ParseMode = "Markdown" 165 | _, _ = bot.Send(msg) 166 | return 167 | } 168 | 169 | // 初始化数据库连接 170 | db.InitDB() 171 | 172 | // 插入域名和对应的运营商 173 | var successCount, failCount int 174 | for i, domain := range domainList { 175 | domain = strings.TrimSpace(domain) 176 | operator := strings.TrimSpace(operatorList[i]) 177 | if domain == "" { 178 | continue 179 | } 180 | if operator == "" { 181 | operator = "未备注" // 默认值 182 | } 183 | 184 | info, err := repository.InsertDomainInfo(primaryDomain, domain, port, operator) 185 | if err != nil { 186 | fmt.Printf("插入域名 %s 失败: %v\n", domain, err) 187 | failCount++ 188 | } else { 189 | fmt.Printf("插入域名 %s 成功: %v\n", domain, info) 190 | successCount++ 191 | } 192 | } 193 | 194 | // 返回操作结果 195 | messageText := fmt.Sprintf("插入完成✅️\n成功: %d 条\n失败: %d 条", successCount, failCount) 196 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 197 | msg.ParseMode = "Markdown" 198 | _, _ = bot.Send(msg) 199 | return 200 | case "version": 201 | fmt.Printf("version命令\n") 202 | v := services.Version() 203 | messageText := fmt.Sprintf(v) // 格式化消息内容,使用 Markdown 格式 204 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 205 | msg.ParseMode = "Markdown" 206 | _, _ = bot.Send(msg) 207 | case "parse": 208 | fmt.Printf("parse命令\n") 209 | if ID != Config.Telegram.Id { 210 | messageText := fmt.Sprintf("`您无法使用parse命令`") // 格式化消息内容,使用 Markdown 格式 211 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 212 | msg.ParseMode = "Markdown" 213 | _, _ = bot.Send(msg) 214 | return 215 | } 216 | // 加载配置文件 217 | db.InitDB() 218 | // 获取所有域名信息 219 | ALLDomain, err := repository.GetDomainInfo() 220 | if err != nil { 221 | fmt.Println("获取域名信息失败:", err) 222 | return 223 | } 224 | 225 | // 存储拼接后的信息 226 | var domainInfoList []string 227 | 228 | // 遍历主域名 229 | for domainName := range ALLDomain { 230 | info, err := services.GetDomainInfo(domainName) 231 | if err != nil { 232 | log.Println("获取域名信息失败:", err) 233 | continue 234 | } 235 | 236 | // 拼接单条域名信息 237 | infoString := fmt.Sprintf("域名:`%s`\n转发域:`%s`\nIP:`%s`\n运营商:`%s`", 238 | info.Domain, info.ForwardingDomain, info.IP, info.ISP) 239 | domainInfoList = append(domainInfoList, infoString) 240 | } 241 | 242 | // 将所有信息拼接成一句话 243 | finalSentence := strings.Join(domainInfoList, "\n----------\n") 244 | 245 | // 输出拼接后的信息 246 | fmt.Println("所有域名信息:", finalSentence) 247 | messageText := fmt.Sprintf("*当前cloudflare的解析*:\n\n" + finalSentence) // 格式化消息内容,使用 Markdown 格式 248 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 249 | msg.ParseMode = "Markdown" 250 | _, _ = bot.Send(msg) 251 | case "getip": 252 | fmt.Printf("getIp命令\n") 253 | if ID != Config.Telegram.Id { 254 | messageText := fmt.Sprintf("`您无法使用getIp命令`") // 格式化消息内容,使用 Markdown 格式 255 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 256 | msg.ParseMode = "Markdown" 257 | _, _ = bot.Send(msg) 258 | return 259 | } 260 | messageText := fmt.Sprintf("`处理进度: %s\n开始写入转发IP...`", "0%") // 格式化消息内容,使用 Markdown 格式 261 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 262 | msg.ParseMode = "Markdown" 263 | sentMessage, _ := bot.Send(msg) 264 | // 连接数据库 265 | db.InitDB() 266 | // 获取所有域名信息 267 | Domains, err := repository.GetALLDomain() 268 | if err != nil { 269 | fmt.Println("获取域名信息失败:", err) 270 | messageText = fmt.Sprintf("`获取域名信息失败`") // 格式化消息内容,使用 Markdown 格式 271 | msg := tgbotapi.NewEditMessageText(update.Message.Chat.ID, sentMessage.MessageID, messageText) 272 | msg.ParseMode = "Markdown" 273 | sentMessage, _ = bot.Send(msg) 274 | return 275 | } 276 | if Domains == nil { 277 | log.Println("没有任何域名数据") 278 | messageText = fmt.Sprintf("`没有任何域名数据`") // 格式化消息内容,使用 Markdown 格式 279 | msg := tgbotapi.NewEditMessageText(update.Message.Chat.ID, sentMessage.MessageID, messageText) 280 | msg.ParseMode = "Markdown" 281 | sentMessage, _ = bot.Send(msg) 282 | return 283 | } 284 | // 获取总域名数量 285 | totalDomains := len(Domains) 286 | 287 | // 遍历 domains 列表 288 | for i, domain := range Domains { 289 | newIP, err := services.ResolveDomainToIP(domain.ForwardingDomain) 290 | if err != nil { 291 | messageText := fmt.Sprintf("域名:`%s`解析IP失败", domain.ForwardingDomain) 292 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 293 | msg.ParseMode = "Markdown" 294 | _, _ = bot.Send(msg) 295 | continue 296 | } 297 | 298 | idStr := fmt.Sprintf("%d", domain.ID) 299 | _, err = repository.UpdateDomainIp(idStr, newIP) 300 | if err != nil { 301 | messageText := fmt.Sprintf("域名:`%s`更新到数据库失败", domain.ForwardingDomain) 302 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, messageText) 303 | msg.ParseMode = "Markdown" 304 | _, _ = bot.Send(msg) 305 | continue 306 | } 307 | 308 | // 计算进度 309 | progress := int(float64(i+1) / float64(totalDomains) * 100) 310 | 311 | // 创建更新的消息内容 312 | var messageText string 313 | if progress == 100 { 314 | messageText = fmt.Sprintf("已完成: `%d%%`\n域名:`%s`\n转发IP:`%s`\n更新成功✅️", progress, domain.ForwardingDomain, newIP) 315 | } else { 316 | messageText = fmt.Sprintf("处理进度: `%d%%`\n域名:`%s`\n转发IP:`%s`\n更新成功✅️", progress, domain.ForwardingDomain, newIP) 317 | } 318 | editProgressMsg := tgbotapi.NewEditMessageText(update.Message.Chat.ID, sentMessage.MessageID, messageText) 319 | editProgressMsg.ParseMode = "Markdown" 320 | _, _ = bot.Send(editProgressMsg) 321 | } 322 | default: 323 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, "抱歉,我不识别这个命令。") 324 | _, _ = bot.Send(msg) 325 | return 326 | } 327 | } 328 | if update.Message.Text != "" { 329 | fmt.Println("收到文本消息") 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /internal/bot/keyboard/key_board.go: -------------------------------------------------------------------------------- 1 | package keyboard 2 | 3 | import ( 4 | "fmt" 5 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 6 | ) 7 | 8 | // Button 单个按钮结构体 9 | type Button struct { 10 | Text string 11 | CallbackData string 12 | } 13 | 14 | // InlineKeyboard 内联键盘结构体,包含二维按钮数组 15 | type InlineKeyboard struct { 16 | Buttons [][]Button 17 | } 18 | 19 | func createInlineKeyboard(keyboard InlineKeyboard) tgbotapi.InlineKeyboardMarkup { 20 | var rows [][]tgbotapi.InlineKeyboardButton 21 | 22 | // 使用 keyboard.Buttons 替代 buttons 23 | for _, buttonRow := range keyboard.Buttons { 24 | var row []tgbotapi.InlineKeyboardButton 25 | for _, button := range buttonRow { 26 | // 使用回调数据作为按钮的回调值 27 | row = append(row, tgbotapi.NewInlineKeyboardButtonData(button.Text, button.CallbackData)) 28 | } 29 | rows = append(rows, row) 30 | } 31 | 32 | // 创建退出按钮并添加到最后一行 33 | exitButton := tgbotapi.NewInlineKeyboardButtonData("退出🔚", "1-exit") 34 | rows = append(rows, []tgbotapi.InlineKeyboardButton{exitButton}) 35 | 36 | return tgbotapi.NewInlineKeyboardMarkup(rows...) 37 | } 38 | 39 | // GenerateMainMenuKeyboard 生成一级菜单按钮 40 | func GenerateMainMenuKeyboard(domainMap map[string]map[string]map[string]interface{}) tgbotapi.InlineKeyboardMarkup { 41 | var keyboard InlineKeyboard 42 | 43 | for domainName, forwardingMap := range domainMap { 44 | for forwardingDomain, details := range forwardingMap { 45 | // 提取端口信息并格式化按钮文本 46 | port := details["Port"] 47 | ban, _ := details["Ban"].(bool) 48 | buttonText := fmt.Sprintf("%s - %s - %v - %t", domainName, forwardingDomain, port, ban) 49 | 50 | // 将回调数据设置为例如 ID 51 | callbackData := fmt.Sprintf("%v", details["ID"]) 52 | 53 | // 创建按钮 54 | button := Button{ 55 | Text: buttonText, 56 | CallbackData: callbackData, 57 | } 58 | 59 | // 将每个按钮作为单独一行(竖向排列) 60 | keyboard.Buttons = append(keyboard.Buttons, []Button{button}) 61 | } 62 | } 63 | 64 | return createInlineKeyboard(keyboard) 65 | } 66 | 67 | // GenerateSubMenuKeyboard 生成二级菜单按钮 68 | func GenerateSubMenuKeyboard(ID uint, Ban bool) *tgbotapi.InlineKeyboardMarkup { 69 | // 设置按钮文本 70 | BanText := "启用中✅️" 71 | if Ban { 72 | BanText = "已封禁🚫️️" 73 | } 74 | 75 | // 定义按钮 76 | buttons := []tgbotapi.InlineKeyboardButton{ 77 | tgbotapi.NewInlineKeyboardButtonData(BanText, fmt.Sprintf("%d-ban", ID)), 78 | tgbotapi.NewInlineKeyboardButtonData("获取转发最新IP🔝", fmt.Sprintf("%d-getIp", ID)), 79 | tgbotapi.NewInlineKeyboardButtonData("解析该条记录📶", fmt.Sprintf("%d-parse", ID)), 80 | tgbotapi.NewInlineKeyboardButtonData("检测并解析该条记录🔄", fmt.Sprintf("%d-checkAndParse", ID)), 81 | tgbotapi.NewInlineKeyboardButtonData("删除该条记录❌️", fmt.Sprintf("%d-del", ID)), 82 | tgbotapi.NewInlineKeyboardButtonData("返回🔙", fmt.Sprintf("%d-back", ID)), 83 | tgbotapi.NewInlineKeyboardButtonData("退出🔚", fmt.Sprintf("%d-exit", ID)), 84 | } 85 | 86 | // 创建竖直排列的键盘 87 | var inlineRows [][]tgbotapi.InlineKeyboardButton 88 | for _, button := range buttons { 89 | inlineRows = append(inlineRows, []tgbotapi.InlineKeyboardButton{button}) 90 | } 91 | 92 | // 返回键盘布局 93 | keyboard := tgbotapi.NewInlineKeyboardMarkup(inlineRows...) 94 | return &keyboard 95 | } 96 | -------------------------------------------------------------------------------- /internal/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "gorm.io/driver/mysql" 6 | "gorm.io/driver/sqlite" 7 | "gorm.io/gorm" 8 | "gorm.io/gorm/logger" 9 | "log" 10 | "telegrambot/config" 11 | "telegrambot/internal/db/models" 12 | "time" 13 | ) 14 | 15 | var DB *gorm.DB 16 | 17 | // InitDB 初始化数据库连接 18 | func InitDB() { 19 | // 加载配置文件 20 | Config, err := config.LoadConfig("") // 加载配置路径 21 | if err != nil { 22 | log.Fatalf("加载配置文件失败: %v", err) 23 | } 24 | 25 | var dsn string 26 | 27 | // 根据配置文件选择 MySQL 或 SQLite 28 | if Config.Database.Type == "mysql" { 29 | // 构造 MySQL DSN 30 | dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s", 31 | Config.Database.User, 32 | Config.Database.Password, 33 | Config.Database.Host, 34 | Config.Database.Port, 35 | Config.Database.Name, 36 | Config.Database.Charset, 37 | ) 38 | } else if Config.Database.Type == "sqlite" { 39 | // 构造 SQLite DSN 40 | dsn = Config.Database.File // SQLite 使用文件路径 41 | } else { 42 | log.Fatalf("不支持的数据库类型: %v", Config.Database.Type) 43 | } 44 | 45 | // 初始化数据库连接 46 | var db *gorm.DB 47 | if Config.Database.Type == "mysql" { 48 | db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ 49 | Logger: logger.Default.LogMode(logger.Info), 50 | }) 51 | } else if Config.Database.Type == "sqlite" { 52 | db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{ 53 | Logger: logger.Default.LogMode(logger.Info), 54 | }) 55 | } 56 | 57 | if err != nil { 58 | log.Fatalf("无法连接到数据库: %v", err) 59 | } 60 | 61 | DB = db 62 | log.Println("数据库连接成功") 63 | 64 | // 设置连接池等(适用于 MySQL,SQLite 没有这么复杂) 65 | if Config.Database.Type == "mysql" { 66 | SetupConnectionPool() 67 | } 68 | } 69 | 70 | // ATInitDB InitDB 初始化数据库连接 71 | func ATInitDB() { 72 | // 加载配置文件 73 | Config, err := config.LoadConfig("") // 加载配置路径 74 | if err != nil { 75 | log.Fatalf("加载配置文件失败: %v", err) 76 | } 77 | 78 | var dsn string 79 | 80 | // 根据配置文件选择 MySQL 或 SQLite 81 | if Config.Database.Type == "mysql" { 82 | // 构造 MySQL DSN 83 | dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s", 84 | Config.Database.User, 85 | Config.Database.Password, 86 | Config.Database.Host, 87 | Config.Database.Port, 88 | Config.Database.Name, 89 | Config.Database.Charset, 90 | ) 91 | } else if Config.Database.Type == "sqlite" { 92 | // 构造 SQLite DSN 93 | dsn = Config.Database.File // SQLite 使用文件路径 94 | } else { 95 | log.Fatalf("不支持的数据库类型: %v", Config.Database.Type) 96 | } 97 | 98 | // 初始化数据库连接 99 | var db *gorm.DB 100 | if Config.Database.Type == "mysql" { 101 | db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ 102 | Logger: logger.Default.LogMode(logger.Info), 103 | }) 104 | } else if Config.Database.Type == "sqlite" { 105 | db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{ 106 | Logger: logger.Default.LogMode(logger.Info), 107 | }) 108 | } 109 | 110 | if err != nil { 111 | log.Fatalf("无法连接到数据库: %v", err) 112 | } 113 | 114 | DB = db 115 | log.Println("数据库连接成功") 116 | 117 | // 设置连接池等(适用于 MySQL,SQLite 没有这么复杂) 118 | if Config.Database.Type == "mysql" { 119 | SetupConnectionPool() 120 | } 121 | AutoMigrate() 122 | } 123 | 124 | // SetupConnectionPool 配置连接池 125 | func SetupConnectionPool() { 126 | sqlDB, err := DB.DB() 127 | if err != nil { 128 | log.Fatalf("获取数据库连接失败: %v", err) 129 | } 130 | 131 | sqlDB.SetMaxOpenConns(1000) 132 | sqlDB.SetMaxIdleConns(100) 133 | sqlDB.SetConnMaxLifetime(5 * time.Minute) 134 | } 135 | 136 | // AutoMigrate 自动迁移数据库模型 137 | func AutoMigrate() { 138 | err := DB.AutoMigrate( 139 | // 添加你的数据模型 140 | &models.Domain{}, 141 | &models.TelegramPermission{}, 142 | ) 143 | if err != nil { 144 | log.Fatalf("自动迁移失败: %v", err) 145 | } 146 | log.Println("数据库模型迁移成功") 147 | } 148 | 149 | // CloseDB 关闭数据库连接 150 | func CloseDB() { 151 | sqlDB, err := DB.DB() 152 | if err != nil { 153 | log.Fatalf("获取数据库连接失败: %v", err) 154 | } 155 | err = sqlDB.Close() 156 | if err != nil { 157 | log.Printf("关闭数据库连接失败: %v", err) 158 | } else { 159 | log.Println("数据库连接已成功关闭") 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /internal/db/models/domain.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Domain 用户数据模型 4 | type Domain struct { 5 | ID uint `gorm:"primaryKey"` // 主键 6 | Domain string `gorm:"size:255"` // 域名 7 | ForwardingDomain string `gorm:"size:255"` // 转发域名 8 | IP string `gorm:"size:255"` // IP地址 9 | Port int `gorm:"size:255"` // 端口 10 | ISP string `gorm:"size:255"` // 运营商 11 | Ban bool `gorm:"default:false"` // 是否启用 12 | } 13 | type TelegramPermission struct { 14 | ID uint `gorm:"primaryKey"` // 主键 15 | TelegramID string `gorm:"size:255;not null"` // TelegramID 16 | IsAdmin bool `gorm:"default:false"` //是否为管理员 17 | ban bool `gorm:"default:false"` // 是否封禁 18 | 19 | } 20 | -------------------------------------------------------------------------------- /internal/db/repository/domain_repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | "gorm.io/gorm" 6 | "log" 7 | "strconv" 8 | "strings" 9 | "telegrambot/internal/db" 10 | "telegrambot/internal/db/models" 11 | ) 12 | 13 | func GetDomainInfo() (map[string]map[string]map[string]interface{}, error) { 14 | // 查询所有数据 15 | var domains []models.Domain 16 | if err := db.DB.Find(&domains).Error; err != nil { 17 | log.Fatalf("查询数据失败: %v", err) 18 | return nil, err 19 | } 20 | 21 | // 如果数据库中没有数据,返回自定义错误 22 | if len(domains) == 0 { 23 | err := fmt.Errorf("数据库中没有域名数据") 24 | log.Println(err) 25 | return nil, err 26 | } 27 | 28 | // 用于存储合并后的结果,map结构:Domain -> ForwardingDomain -> 详情 29 | domainMap := make(map[string]map[string]map[string]interface{}) 30 | 31 | // 遍历所有数据并进行合并 32 | for _, domain := range domains { 33 | // 如果该 Domain 不存在,初始化它 34 | if _, exists := domainMap[domain.Domain]; !exists { 35 | domainMap[domain.Domain] = make(map[string]map[string]interface{}) 36 | } 37 | 38 | // 如果该 ForwardingDomain 不存在,初始化它 39 | if _, exists := domainMap[domain.Domain][domain.ForwardingDomain]; !exists { 40 | domainMap[domain.Domain][domain.ForwardingDomain] = make(map[string]interface{}) 41 | } 42 | 43 | // 将 ID, IP, Port, ISP, Ban 添加到合适的位置 44 | domainMap[domain.Domain][domain.ForwardingDomain]["ID"] = domain.ID 45 | domainMap[domain.Domain][domain.ForwardingDomain]["IP"] = domain.IP 46 | domainMap[domain.Domain][domain.ForwardingDomain]["Port"] = domain.Port 47 | domainMap[domain.Domain][domain.ForwardingDomain]["ISP"] = domain.ISP 48 | domainMap[domain.Domain][domain.ForwardingDomain]["Ban"] = domain.Ban 49 | } 50 | 51 | return domainMap, nil 52 | } 53 | 54 | func GetDomainIDInfo(ID string) (domainInfo models.Domain, err error) { 55 | var domain models.Domain 56 | // 初始化默认值 57 | var numericID = ID 58 | 59 | // 检查并提取 ID 的数字部分(如果包含 "-") 60 | if strings.Contains(ID, "-") { 61 | idParts := strings.Split(ID, "-") 62 | if len(idParts) > 0 { 63 | numericID = idParts[0] // 提取 "-" 前的部分 64 | } 65 | } 66 | // 将字符串ID转换为uint类型 67 | uintID, err := strconv.ParseUint(numericID, 10, 32) 68 | if err != nil { 69 | fmt.Printf("无效的ID格式: %v\n", err) 70 | return domain, err 71 | } 72 | 73 | // 根据ID查询 74 | result := db.DB.First(&domain, uint(uintID)) 75 | if result.Error != nil { 76 | if result.RowsAffected == 0 { 77 | fmt.Println("未找到记录") 78 | } else { 79 | fmt.Printf("查询错误: %v\n", result.Error) 80 | } 81 | return domain, err 82 | } 83 | 84 | // 输出查询结果 85 | //fmt.Printf("查询结果: %+v\n", domain) 86 | return domain, nil 87 | } 88 | 89 | func UpdateDomainIp(ID string, newIP string) (models.Domain, error) { 90 | // 初始化默认值 91 | var numericID = ID 92 | 93 | // 检查并提取 ID 的数字部分(如果包含 "-") 94 | if strings.Contains(ID, "-") { 95 | idParts := strings.Split(ID, "-") 96 | if len(idParts) > 0 { 97 | numericID = idParts[0] // 提取 "-" 前的部分 98 | } 99 | } 100 | // 转换字符串 ID 为 uint 类型 101 | uintID, err := strconv.ParseUint(numericID, 10, 32) // 将字符串ID转换为uint类型 102 | if err != nil { 103 | fmt.Printf("无效的ID格式: %v\n", err) 104 | return models.Domain{}, err 105 | } 106 | 107 | // 查找目标域名记录 108 | var domain models.Domain 109 | result := db.DB.First(&domain, uint(uintID)) 110 | if result.Error != nil { 111 | fmt.Printf("查询失败: %v\n", result.Error) 112 | return models.Domain{}, result.Error 113 | } 114 | 115 | // 更新IP地址 116 | domain.IP = newIP 117 | updateResult := db.DB.Save(&domain) 118 | if updateResult.Error != nil { 119 | fmt.Printf("更新失败: %v\n", updateResult.Error) 120 | return models.Domain{}, updateResult.Error 121 | } 122 | 123 | // 返回更新后的记录 124 | return domain, nil 125 | } 126 | 127 | func DeleteDomainByID(ID string) (models.Domain, error) { 128 | var domain models.Domain 129 | // 初始化默认值 130 | var numericID = ID 131 | 132 | // 检查并提取 ID 的数字部分(如果包含 "-") 133 | if strings.Contains(ID, "-") { 134 | idParts := strings.Split(ID, "-") 135 | if len(idParts) > 0 { 136 | numericID = idParts[0] // 提取 "-" 前的部分 137 | } 138 | } 139 | // 将字符串ID转换为uint类型 140 | uintID, err := strconv.ParseUint(numericID, 10, 32) 141 | if err != nil { 142 | fmt.Printf("无效的ID格式: %v\n", err) 143 | return domain, err 144 | } 145 | 146 | // 根据ID查询 147 | result := db.DB.First(&domain, uint(uintID)) 148 | if result.Error != nil { 149 | if result.RowsAffected == 0 { 150 | fmt.Println("未找到记录") 151 | } else { 152 | fmt.Printf("查询错误: %v\n", result.Error) 153 | } 154 | return domain, err 155 | } 156 | 157 | // 删除记录 158 | deleteResult := db.DB.Delete(&domain) 159 | if deleteResult.Error != nil { 160 | fmt.Printf("删除错误: %v\n", deleteResult.Error) 161 | return domain, deleteResult.Error 162 | } 163 | 164 | // 输出删除结果 165 | fmt.Println("记录已删除") 166 | return domain, nil 167 | } 168 | 169 | func UpdateDomainBan(ID string, Ban bool) (models.Domain, error) { 170 | // 初始化默认值 171 | var numericID = ID 172 | 173 | // 检查并提取 ID 的数字部分(如果包含 "-") 174 | if strings.Contains(ID, "-") { 175 | idParts := strings.Split(ID, "-") 176 | if len(idParts) > 0 { 177 | numericID = idParts[0] // 提取 "-" 前的部分 178 | } 179 | } 180 | // 转换字符串 ID 为 uint 类型 181 | uintID, err := strconv.ParseUint(numericID, 10, 32) // 将字符串ID转换为uint类型 182 | if err != nil { 183 | fmt.Printf("无效的ID格式: %v\n", err) 184 | return models.Domain{}, err 185 | } 186 | 187 | // 查找目标域名记录 188 | var domain models.Domain 189 | result := db.DB.First(&domain, uint(uintID)) 190 | if result.Error != nil { 191 | fmt.Printf("查询失败: %v\n", result.Error) 192 | return models.Domain{}, result.Error 193 | } 194 | 195 | // 更新IP地址 196 | domain.Ban = Ban 197 | updateResult := db.DB.Save(&domain) 198 | if updateResult.Error != nil { 199 | fmt.Printf("更新失败: %v\n", updateResult.Error) 200 | return models.Domain{}, updateResult.Error 201 | } 202 | 203 | // 返回更新后的记录 204 | return domain, nil 205 | } 206 | 207 | func InsertDomainInfo(Domain string, ForwardingDomain string, Port int, ISP string) (models.Domain, error) { 208 | // 先查询数据库,检查是否存在相同的 ForwardingDomain 和 Port 组合 209 | var existingDomain models.Domain 210 | if err := db.DB.Where("Domain = ? AND forwarding_domain = ? AND port = ?", Domain, ForwardingDomain, Port).First(&existingDomain).Error; err == nil { 211 | // 如果存在记录,则返回错误,表示该记录已经存在 212 | return models.Domain{}, fmt.Errorf("已存在域名 '%s' 转发域名 '%s' and 端口 '%d", Domain, ForwardingDomain, Port) 213 | } else if err != gorm.ErrRecordNotFound { 214 | // 如果发生了其他错误(非记录未找到),则返回错误 215 | return models.Domain{}, fmt.Errorf("非记录未找到info: %v", err) 216 | } 217 | DomainInfo := models.Domain{ 218 | Domain: Domain, 219 | ForwardingDomain: ForwardingDomain, 220 | Port: Port, 221 | IP: "0", 222 | ISP: ISP, 223 | Ban: false, 224 | } 225 | // 将新的记录插入数据库 226 | if err := db.DB.Create(&DomainInfo).Error; err != nil { 227 | return models.Domain{}, fmt.Errorf("插入数据库失败info: %v", err) 228 | } 229 | 230 | return DomainInfo, nil 231 | } 232 | 233 | func GetDomainInfoByIp(Domain string, ip string) (domainInfo models.Domain, err error) { 234 | domain := models.Domain{ 235 | Domain: Domain, 236 | ForwardingDomain: "", 237 | IP: ip, 238 | Port: 0, 239 | ISP: "", 240 | } 241 | 242 | // 根据域名和IP查询记录 243 | result := db.DB.Where("domain = ? AND ip = ?", Domain, ip).First(&domain) 244 | if result.Error != nil { 245 | if result.RowsAffected == 0 { 246 | fmt.Println("未找到记录") 247 | } else { 248 | fmt.Printf("查询错误: %v\n", result.Error) 249 | } 250 | return domain, fmt.Errorf("查询错误: %v", err) 251 | } 252 | 253 | // 输出查询结果 254 | //fmt.Printf("查询结果: %+v\n", domain) 255 | return domain, nil 256 | } 257 | 258 | func GetALLDomain() ([]models.Domain, error) { 259 | // 查询所有数据 260 | var domains []models.Domain 261 | if err := db.DB.Find(&domains).Error; err != nil { 262 | // 记录日志,但不终止程序 263 | log.Printf("查询数据失败: %v", err) 264 | return nil, err 265 | } 266 | 267 | // 检查是否查询到结果 268 | if len(domains) == 0 { 269 | log.Println("没有找到任何域名数据") 270 | return nil, nil 271 | } 272 | 273 | return domains, nil 274 | } 275 | -------------------------------------------------------------------------------- /internal/services/check.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 6 | "net" 7 | "os" 8 | "os/exec" 9 | "runtime" 10 | "telegrambot/config" 11 | "telegrambot/internal/db" 12 | "telegrambot/internal/db/repository" 13 | "time" 14 | ) 15 | 16 | // CheckTCPConnectivity 对指定地址进行5次TCP连接测试,如果至少一次成功,则返回true,否则返回false 17 | func CheckTCPConnectivity(ip string, port int) bool { 18 | address := fmt.Sprintf("%s:%d", ip, port) 19 | success := false 20 | // 加载配置文件 21 | Config, err := config.LoadConfig("") 22 | if err != nil { 23 | fmt.Printf("加载配置文件失败: %v", err) 24 | } 25 | for i := 0; i < 5; i++ { 26 | conn, err := net.DialTimeout("tcp", address, time.Duration(Config.Check.IpCheckTime)*time.Second) 27 | if err == nil { 28 | success = true 29 | _ = conn.Close() // 关闭连接 30 | fmt.Printf("IP检测正常:%s\n", ip) 31 | break // 如果成功一次就可以退出循环 32 | } 33 | time.Sleep(1 * time.Second) // 每次尝试间隔1秒 34 | fmt.Printf("IP检测异常:%s---异常次数:%d\n", ip, i+1) 35 | } 36 | return success 37 | } 38 | 39 | // ResolveDomainToIP 解析域名并返回第一个IP地址 40 | func ResolveDomainToIP(domain string) (string, error) { 41 | // 使用 net.LookupIP 解析域名 42 | ips, err := net.LookupIP(domain) 43 | if err != nil { 44 | return "", err 45 | } 46 | 47 | if len(ips) > 0 { 48 | return ips[0].String(), nil 49 | } 50 | 51 | return "", fmt.Errorf("未找到任何IP地址") 52 | } 53 | 54 | func ALLCheckTCPConnectivity(bot *tgbotapi.BotAPI, update tgbotapi.Update, shouldSend bool) bool { 55 | db.InitDB() // 连接数据库 56 | // 调用清除 DNS 缓存的函数 57 | if err := ClearDNSCache(); err != nil { 58 | fmt.Println("错误:", err) 59 | } 60 | ALLDomain, err := repository.GetDomainInfo() 61 | if err != nil { 62 | fmt.Println("获取域名信息失败:", err) 63 | return false 64 | } 65 | 66 | sendOrEditMessage := func(chatID int64, text string, messageID *int, isEdit bool, forceSend bool) { 67 | // 如果是静默模式但 forceSend 为 true,强制发送消息 68 | if shouldSend || forceSend { 69 | if isEdit { 70 | if *messageID == 0 { 71 | // 如果 messageID 为 0,尝试发送新消息 72 | fmt.Println("错误: 尝试编辑消息,但 messageID 为 0,发送新消息") 73 | msg := tgbotapi.NewMessage(chatID, text) 74 | msg.ParseMode = "Markdown" 75 | sentMsg, err := bot.Send(msg) 76 | if err != nil { 77 | fmt.Printf("发送新消息失败: %v\n", err) 78 | return 79 | } 80 | *messageID = sentMsg.MessageID // 更新 messageID 为发送的消息ID 81 | } else { 82 | // 编辑已有消息 83 | editMsg := tgbotapi.NewEditMessageText(chatID, *messageID, text) 84 | editMsg.ParseMode = "Markdown" 85 | if _, err := bot.Send(editMsg); err != nil { 86 | fmt.Printf("编辑消息失败: %v\n", err) 87 | } 88 | } 89 | } else { 90 | // 发送新消息 91 | msg := tgbotapi.NewMessage(chatID, text) 92 | msg.ParseMode = "Markdown" 93 | sentMsg, err := bot.Send(msg) 94 | if err != nil { 95 | fmt.Printf("发送消息失败: %v\n", err) 96 | return 97 | } 98 | *messageID = sentMsg.MessageID // 更新 messageID 为发送的消息ID 99 | } 100 | } 101 | } 102 | 103 | // 遍历主域名 104 | for domainName, forwardingMap := range ALLDomain { 105 | var port int 106 | for _, details := range forwardingMap { 107 | if value, ok := details["Port"].(int); ok { 108 | port = value 109 | } 110 | break 111 | } 112 | // 主域名连通性检测 113 | var messageID int 114 | DomainIP, err := ResolveDomainToIP(domainName) 115 | if err != nil { 116 | fmt.Printf("主域名未进行配置解析: %s\n", err) 117 | sendOrEditMessage(update.Message.Chat.ID, fmt.Sprintf("*主域名未进行配置解析记录,请先进行解析:*`%s`", domainName), &messageID, false, false) 118 | continue 119 | } 120 | 121 | sendOrEditMessage(update.Message.Chat.ID, fmt.Sprintf("*开始检测域名:*`%s:%d`", domainName, port), &messageID, false, false) 122 | fmt.Printf("开始检测域名:%s:%d\n", domainName, port) 123 | if isConnected := CheckTCPConnectivity(DomainIP, port); isConnected { 124 | sendOrEditMessage(update.Message.Chat.ID, fmt.Sprintf("*节点:*`%s:%d`*正常*", domainName, port), &messageID, true, false) 125 | continue 126 | } else { 127 | sendOrEditMessage(update.Message.Chat.ID, fmt.Sprintf("*节点:*`%s:%d`*无法连通*", domainName, port), &messageID, true, true) 128 | } 129 | 130 | // 检测子域名连通性 131 | var forwardingDomainInfo string 132 | for forwardingDomain, details := range forwardingMap { 133 | sendOrEditMessage(update.Message.Chat.ID, fmt.Sprintf("*开始检测转发域名:*`%s:%d`", forwardingDomain, port), &messageID, true, false) 134 | ban, _ := details["Ban"].(bool) 135 | if ban { 136 | msg := fmt.Sprintf("-----\n*转发域名封禁:*`%s`, *IsBan:*`%t`\n", forwardingDomain, ban) 137 | fmt.Printf(msg) 138 | sendOrEditMessage(update.Message.Chat.ID, fmt.Sprintf(msg), &messageID, true, true) 139 | // 将消息存储到 forwardingDomainInfo 中 140 | forwardingDomainInfo += msg 141 | continue 142 | } 143 | 144 | forwardingIP, err := ResolveDomainToIP(forwardingDomain) 145 | if err != nil { 146 | msg := fmt.Sprintf("-----\n*转发域名解析错误:* `%s`, 错误: %s\n", forwardingDomain, err) 147 | fmt.Printf(msg) 148 | sendOrEditMessage(update.Message.Chat.ID, fmt.Sprintf(msg), &messageID, false, true) 149 | forwardingDomainInfo += msg 150 | continue 151 | } 152 | fmt.Printf("开始检测转发域名:%s:%d\n", forwardingDomain, port) 153 | if isConnected := CheckTCPConnectivity(forwardingIP, port); isConnected { 154 | if _, err := UpdateARecord(domainName, forwardingIP); err != nil { 155 | fmt.Printf("更新域名 A 记录失败: %s\n", err) 156 | continue 157 | } 158 | if forwardingDomainInfo != "" { 159 | msg := fmt.Sprintf("*域名A记录:*`%s`\n*转发域名:*`%s\n`*解析IP:*`%s`\n*运营商:*`%s`\n=====*异常的转发域:*=====\n%s=====", domainName, forwardingDomain, forwardingIP, details["ISP"], forwardingDomainInfo) 160 | sendOrEditMessage(update.Message.Chat.ID, msg, &messageID, true, true) 161 | } else { 162 | msg := fmt.Sprintf("*域名A记录:*`%s`\n*转发域名:*`%s\n`*解析IP:*`%s`\n*运营商:*`%s`", domainName, forwardingDomain, forwardingIP, details["ISP"]) 163 | sendOrEditMessage(update.Message.Chat.ID, msg, &messageID, true, true) 164 | } 165 | 166 | ID := fmt.Sprintf("%v", details["ID"]) 167 | if _, err := repository.UpdateDomainIp(ID, forwardingIP); err != nil { 168 | fmt.Printf("更新数据库失败: %s\n", err) 169 | } else { 170 | fmt.Printf("数据库更新成功: %s -> %s\n", forwardingDomain, forwardingIP) 171 | } 172 | break 173 | } else { 174 | msg := fmt.Sprintf("-----\n*转发域名异常:* `%s:%d`\n", forwardingDomain, port) 175 | fmt.Printf(msg) 176 | forwardingDomainInfo += msg 177 | } 178 | } 179 | } 180 | 181 | fmt.Println("所有域名检测完毕") 182 | return true 183 | } 184 | 185 | // ClearDNSCache 根据操作系统清除 DNS 缓存 186 | func ClearDNSCache() error { 187 | var cmd *exec.Cmd 188 | 189 | switch runtime.GOOS { 190 | case "windows": 191 | // Windows 系统清除 DNS 缓存 192 | cmd = exec.Command("ipconfig", "/flushdns") 193 | case "linux": 194 | // Linux 系统清除 DNS 缓存 195 | // 检查 systemd 是否存在 196 | cmd = exec.Command("sudo", "systemctl", "restart", "systemd-resolved") 197 | case "darwin": // macOS 198 | // macOS 系统清除 DNS 缓存 199 | cmd = exec.Command("sudo", "killall", "-HUP", "mDNSResponder") 200 | default: 201 | return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS) 202 | } 203 | // 执行命令 204 | cmd.Stdout = os.Stdout 205 | cmd.Stderr = os.Stderr 206 | if err := cmd.Run(); err != nil { 207 | return fmt.Errorf("清除 DNS 缓存失败: %v", err) 208 | } 209 | 210 | fmt.Println("DNS 缓存已成功清除") 211 | return nil 212 | } 213 | -------------------------------------------------------------------------------- /internal/services/cloudflare.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cloudflare/cloudflare-go" 7 | "log" 8 | "strings" 9 | "telegrambot/config" 10 | "telegrambot/internal/db/models" 11 | "telegrambot/internal/db/repository" 12 | ) 13 | 14 | // UpdateARecord 更新 A 记录的函数,目前支持根域名与二级域名 15 | func UpdateARecord(fullDomain, ip string) (string, error) { 16 | // 创建 Cloudflare 客户端 17 | Config, err := config.LoadConfig("") 18 | if err != nil { 19 | log.Panic(err) 20 | } 21 | key := Config.Cloudflare.Key // 替换为你的 Cloudflare API 密钥 22 | email := Config.Cloudflare.Email // 替换为你的 Cloudflare 电子邮件地址 23 | client, err := cloudflare.New(key, email) 24 | if err != nil { 25 | return "", fmt.Errorf("创建 Cloudflare 客户端失败: %v", err) 26 | } 27 | ctx := context.Background() 28 | 29 | // 分割域名为子域名和主域名 30 | parts := strings.Split(fullDomain, ".") 31 | if len(parts) < 2 { 32 | return "", fmt.Errorf("无效的域名: %s", fullDomain) 33 | } 34 | 35 | // 判断是否为根域名(example.com) 36 | var subdomain string 37 | if len(parts) == 2 { 38 | subdomain = "" // 根域名没有子域名 39 | } else { 40 | subdomain = strings.Join(parts[:len(parts)-2], ".") 41 | } 42 | domain := parts[len(parts)-2] + "." + parts[len(parts)-1] 43 | 44 | // 获取域名的 Zone ID 45 | zones, err := client.ListZones(ctx) 46 | if err != nil { 47 | return "", fmt.Errorf("获取 Zone 列表失败: %v", err) 48 | } 49 | 50 | var zoneID string 51 | for _, zone := range zones { 52 | if zone.Name == domain { 53 | zoneID = zone.ID 54 | break 55 | } 56 | } 57 | 58 | if zoneID == "" { 59 | return "", fmt.Errorf("未找到主域名 %s 对应的 Zone ID", domain) 60 | } 61 | 62 | // 列出 DNS 记录 63 | rc := &cloudflare.ResourceContainer{ 64 | Level: "zone", 65 | Identifier: zoneID, 66 | } 67 | DNSRecords, _, err := client.ListDNSRecords(ctx, rc, cloudflare.ListDNSRecordsParams{}) 68 | if err != nil { 69 | return "", fmt.Errorf("列出 DNS 记录失败: %v", err) 70 | } 71 | 72 | // 输出所有 DNS 记录,帮助调试 73 | for _, record := range DNSRecords { 74 | fmt.Printf("记录:ID=%s, 名称=%s, 类型=%s, 内容=%s\n", record.ID, record.Name, record.Type, record.Content) 75 | } 76 | 77 | var recordID string 78 | for _, record := range DNSRecords { 79 | // 如果是根域名或者完整匹配子域名 80 | if (subdomain == "" && record.Name == domain) || record.Name == fullDomain { 81 | recordID = record.ID 82 | fmt.Printf("匹配的记录ID: %s\n", recordID) // 输出匹配的记录ID 83 | break 84 | } 85 | } 86 | 87 | // 如果没有找到记录 88 | if recordID == "" { 89 | return "", fmt.Errorf("未找到匹配的 DNS 记录:%s", fullDomain) 90 | } 91 | 92 | // 更新 DNS 记录 93 | params := cloudflare.UpdateDNSRecordParams{ 94 | Type: "A", 95 | Name: fullDomain, 96 | Content: ip, 97 | TTL: 60, 98 | ID: recordID, // 确保 ID 被正确设置 99 | } 100 | _, err = client.UpdateDNSRecord(ctx, rc, params) 101 | if err != nil { 102 | return "", fmt.Errorf("更新 DNS 记录失败: %v", err) 103 | } 104 | 105 | // 返回成功信息 106 | return "域名解析成功", nil 107 | } 108 | 109 | func GetDomainInfo(fullDomain string) (models.Domain, error) { 110 | var Domain models.Domain 111 | // 创建 Cloudflare 客户端 112 | Config, err := config.LoadConfig("") 113 | if err != nil { 114 | log.Panic(err) 115 | } 116 | key := Config.Cloudflare.Key // 替换为你的 Cloudflare API 密钥 117 | email := Config.Cloudflare.Email // 替换为你的 Cloudflare 电子邮件地址 118 | client, err := cloudflare.New(key, email) 119 | if err != nil { 120 | return Domain, fmt.Errorf("创建 Cloudflare 客户端失败: %v", err) 121 | } 122 | ctx := context.Background() 123 | 124 | // 分割域名为子域名和主域名 125 | parts := strings.Split(fullDomain, ".") 126 | if len(parts) < 2 { 127 | return Domain, fmt.Errorf("无效的域名: %s", fullDomain) 128 | } 129 | 130 | // 判断是否为根域名(example.com) 131 | var subdomain string 132 | if len(parts) == 2 { 133 | subdomain = "" // 根域名没有子域名 134 | } else { 135 | subdomain = strings.Join(parts[:len(parts)-2], ".") 136 | } 137 | domain := parts[len(parts)-2] + "." + parts[len(parts)-1] 138 | 139 | // 获取域名的 Zone ID 140 | zones, err := client.ListZones(ctx) 141 | if err != nil { 142 | return Domain, fmt.Errorf("获取 Zone 列表失败: %v", err) 143 | } 144 | 145 | var zoneID string 146 | for _, zone := range zones { 147 | if zone.Name == domain { 148 | zoneID = zone.ID 149 | break 150 | } 151 | } 152 | 153 | if zoneID == "" { 154 | return Domain, fmt.Errorf("未找到主域名 %s 对应的 Zone ID", domain) 155 | } 156 | 157 | // 列出 DNS 记录 158 | rc := &cloudflare.ResourceContainer{ 159 | Level: "zone", 160 | Identifier: zoneID, 161 | } 162 | DNSRecords, _, err := client.ListDNSRecords(ctx, rc, cloudflare.ListDNSRecordsParams{}) 163 | if err != nil { 164 | return Domain, fmt.Errorf("列出 DNS 记录失败: %v", err) 165 | } 166 | 167 | // 输出所有 DNS 记录,帮助调试 168 | //for _, record := range DNSRecords { 169 | //fmt.Printf("记录:ID=%s, 名称=%s, 类型=%s, 解析内容=%s\n", record.ID, record.Name, record.Type, record.Content) 170 | //} 171 | 172 | var recordID string 173 | var recordIP string 174 | for _, record := range DNSRecords { 175 | // 如果是根域名或者完整匹配子域名 176 | if (subdomain == "" && record.Name == domain) || record.Name == fullDomain { 177 | recordID = record.ID 178 | recordIP = record.Content 179 | fmt.Printf("匹配的记录ID: %s\n", recordID) // 输出匹配的记录ID 180 | break 181 | } 182 | } 183 | // 如果没有找到记录 184 | if recordID == "" { 185 | return Domain, fmt.Errorf("未找到匹配的 DNS 记录:%s", fullDomain) 186 | } 187 | // 进行数据库查询根据IP查询 188 | fmt.Printf(recordIP) 189 | DomainInfo, err := repository.GetDomainInfoByIp(fullDomain, recordIP) 190 | if err != nil { 191 | return Domain, err 192 | } 193 | return DomainInfo, nil 194 | } 195 | -------------------------------------------------------------------------------- /internal/services/version.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | func Version() (v string) { 4 | return "`当前BOT运行版本:1.0.5`" 5 | } 6 | -------------------------------------------------------------------------------- /internal/utils/listen.go: -------------------------------------------------------------------------------- 1 | package utils 2 | -------------------------------------------------------------------------------- /internal/utils/logger.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | //日志工具 4 | -------------------------------------------------------------------------------- /internal/utils/tool.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | func ValidateFormat(params string) (bool, error) { 11 | // 使用 "#" 拆分参数 12 | parts := strings.Split(params, "#") 13 | 14 | // 检查拆分后的部分数量是否为 4 15 | if len(parts) != 4 { 16 | return false, fmt.Errorf("格式不正确请确保只有4个#号当前#号个数:%d", len(parts)) 17 | } 18 | 19 | // 验证第一部分是否为有效域名格式(简单检查) 20 | domain := parts[0] 21 | if !isValidDomain(domain) { 22 | return false, fmt.Errorf("域名不合法,请用合法的域名格式,如www.baidu.com\n您当前传入的非法格式域名: %s", domain) 23 | } 24 | 25 | // 验证第三部分是否为整数(例如:0) 26 | param3 := parts[2] 27 | if _, err := strconv.Atoi(param3); err != nil { 28 | return false, fmt.Errorf("端口为非整数,请输入整数端口如7890\n您当前传入的非法格式端口: %s", param3) 29 | } 30 | 31 | // 如果所有验证都通过 32 | return true, nil 33 | } 34 | 35 | // isValidDomain 验证域名格式是否正确(支持根域名和二级域名) 36 | func isValidDomain(domain string) bool { 37 | // 正则表达式检查: 根域名 或 二级域名 38 | // 举例: example.com, sub.example.com 39 | regex := `^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}$` 40 | match, err := regexp.MatchString(regex, domain) 41 | if err != nil { 42 | // 如果正则匹配出错,认为域名无效 43 | return false 44 | } 45 | return match 46 | } 47 | -------------------------------------------------------------------------------- /photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reppoor/telegram-bot-ddns/c0c7b5ceaca88c49cf98f0b6c073a7d2d02bb8ff/photo.jpg -------------------------------------------------------------------------------- /tests/services_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "telegrambot/config" 7 | "testing" 8 | ) 9 | 10 | // 更新 A 记录的函数 11 | func TestRepository(t *testing.T) { 12 | // 加载配置文件 13 | Config, err := config.LoadConfig("") 14 | if err != nil { 15 | log.Fatalf("加载配置文件失败: %v", err) 16 | } 17 | fmt.Printf(Config.Database.Host, "") 18 | } 19 | --------------------------------------------------------------------------------