├── .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 | 
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 | [](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 |
--------------------------------------------------------------------------------