mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-05-06 13:48:52 +00:00
Compare commits
2 Commits
b4a0641177
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3319d7344f | |||
| c606fd99f6 |
-134
@@ -1,134 +0,0 @@
|
|||||||
import os
|
|
||||||
import time
|
|
||||||
import requests
|
|
||||||
import hashlib
|
|
||||||
from github import Github
|
|
||||||
|
|
||||||
|
|
||||||
def get_github_latest_release():
|
|
||||||
g = Github()
|
|
||||||
repo = g.get_repo("nezhahq/nezha")
|
|
||||||
release = repo.get_latest_release()
|
|
||||||
if release:
|
|
||||||
print(f"Latest release tag is: {release.tag_name}")
|
|
||||||
print(f"Latest release info is: {release.body}")
|
|
||||||
files = []
|
|
||||||
for asset in release.get_assets():
|
|
||||||
url = asset.browser_download_url
|
|
||||||
name = asset.name
|
|
||||||
|
|
||||||
response = requests.get(url)
|
|
||||||
if response.status_code == 200:
|
|
||||||
with open(name, 'wb') as f:
|
|
||||||
f.write(response.content)
|
|
||||||
print(f"Downloaded {name}")
|
|
||||||
else:
|
|
||||||
print(f"Failed to download {name}")
|
|
||||||
file_abs_path = get_abs_path(asset.name)
|
|
||||||
files.append(file_abs_path)
|
|
||||||
sync_to_gitee(release.tag_name, release.body, files)
|
|
||||||
else:
|
|
||||||
print("No releases found.")
|
|
||||||
|
|
||||||
|
|
||||||
def delete_gitee_releases(latest_id, client, uri, token):
|
|
||||||
get_data = {
|
|
||||||
'access_token': token
|
|
||||||
}
|
|
||||||
|
|
||||||
release_info = []
|
|
||||||
release_response = client.get(uri, json=get_data)
|
|
||||||
if release_response.status_code == 200:
|
|
||||||
release_info = release_response.json()
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f"Request failed with status code {release_response.status_code}")
|
|
||||||
|
|
||||||
release_ids = []
|
|
||||||
for block in release_info:
|
|
||||||
if 'id' in block:
|
|
||||||
release_ids.append(block['id'])
|
|
||||||
|
|
||||||
print(f'Current release ids: {release_ids}')
|
|
||||||
release_ids.remove(latest_id)
|
|
||||||
|
|
||||||
for id in release_ids:
|
|
||||||
release_uri = f"{uri}/{id}"
|
|
||||||
delete_data = {
|
|
||||||
'access_token': token
|
|
||||||
}
|
|
||||||
delete_response = client.delete(release_uri, json=delete_data)
|
|
||||||
if delete_response.status_code == 204:
|
|
||||||
print(f'Successfully deleted release #{id}.')
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Request failed with status code {delete_response.status_code}")
|
|
||||||
|
|
||||||
|
|
||||||
def sync_to_gitee(tag: str, body: str, files: slice):
|
|
||||||
release_id = ""
|
|
||||||
owner = "naibahq"
|
|
||||||
repo = "nezha"
|
|
||||||
release_api_uri = f"https://gitee.com/api/v5/repos/{owner}/{repo}/releases"
|
|
||||||
api_client = requests.Session()
|
|
||||||
api_client.headers.update({
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
})
|
|
||||||
|
|
||||||
access_token = os.environ['GITEE_TOKEN']
|
|
||||||
release_data = {
|
|
||||||
'access_token': access_token,
|
|
||||||
'tag_name': tag,
|
|
||||||
'name': tag,
|
|
||||||
'body': body,
|
|
||||||
'prerelease': False,
|
|
||||||
'target_commitish': 'master'
|
|
||||||
}
|
|
||||||
release_api_response = api_client.post(release_api_uri, json=release_data)
|
|
||||||
if release_api_response.status_code == 201:
|
|
||||||
release_info = release_api_response.json()
|
|
||||||
release_id = release_info.get('id')
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f"Request failed with status code {release_api_response.status_code}")
|
|
||||||
|
|
||||||
print(f"Gitee release id: {release_id}")
|
|
||||||
asset_api_uri = f"{release_api_uri}/{release_id}/attach_files"
|
|
||||||
|
|
||||||
for file_path in files:
|
|
||||||
success = False
|
|
||||||
|
|
||||||
while not success:
|
|
||||||
files = {
|
|
||||||
'file': open(file_path, 'rb')
|
|
||||||
}
|
|
||||||
|
|
||||||
asset_api_response = requests.post(
|
|
||||||
asset_api_uri, params={'access_token': access_token}, files=files)
|
|
||||||
|
|
||||||
if asset_api_response.status_code == 201:
|
|
||||||
asset_info = asset_api_response.json()
|
|
||||||
asset_name = asset_info.get('name')
|
|
||||||
print(f"Successfully uploaded {asset_name}!")
|
|
||||||
success = True
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f"Request failed with status code {asset_api_response.status_code}")
|
|
||||||
|
|
||||||
# 仅保留最新 Release 以防超出 Gitee 仓库配额
|
|
||||||
try:
|
|
||||||
delete_gitee_releases(release_id, api_client, release_api_uri, access_token)
|
|
||||||
except ValueError as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
api_client.close()
|
|
||||||
print("Sync is completed!")
|
|
||||||
|
|
||||||
|
|
||||||
def get_abs_path(path: str):
|
|
||||||
wd = os.getcwd()
|
|
||||||
return os.path.join(wd, path)
|
|
||||||
|
|
||||||
|
|
||||||
get_github_latest_release()
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import os
|
|
||||||
import time
|
|
||||||
import requests
|
|
||||||
from github import Github
|
|
||||||
|
|
||||||
ATOMGIT_API = "https://api.atomgit.com/api/v5"
|
|
||||||
ATOMGIT_OWNER = "naiba"
|
|
||||||
ATOMGIT_REPO = "nezha-dashboard"
|
|
||||||
GITHUB_REPO = "nezhahq/nezha"
|
|
||||||
|
|
||||||
|
|
||||||
def get_github_latest_release():
|
|
||||||
g = Github()
|
|
||||||
repo = g.get_repo(GITHUB_REPO)
|
|
||||||
release = repo.get_latest_release()
|
|
||||||
if not release:
|
|
||||||
print("No releases found.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"Latest release tag is: {release.tag_name}")
|
|
||||||
print(f"Latest release info is: {release.body}")
|
|
||||||
files = []
|
|
||||||
for asset in release.get_assets():
|
|
||||||
url = asset.browser_download_url
|
|
||||||
name = asset.name
|
|
||||||
|
|
||||||
response = requests.get(url)
|
|
||||||
if response.status_code == 200:
|
|
||||||
with open(name, "wb") as f:
|
|
||||||
f.write(response.content)
|
|
||||||
print(f"Downloaded {name}")
|
|
||||||
else:
|
|
||||||
print(f"Failed to download {name}")
|
|
||||||
files.append(get_abs_path(name))
|
|
||||||
sync_to_atomgit(release.tag_name, release.body, files)
|
|
||||||
|
|
||||||
|
|
||||||
def sync_to_atomgit(tag, body, files):
|
|
||||||
access_token = os.environ["ATOMGIT_PAT"]
|
|
||||||
release_api_uri = f"{ATOMGIT_API}/repos/{ATOMGIT_OWNER}/{ATOMGIT_REPO}/releases"
|
|
||||||
|
|
||||||
auth_headers = {"Authorization": f"Bearer {access_token}"}
|
|
||||||
release_data = {
|
|
||||||
"tag_name": tag,
|
|
||||||
"name": tag,
|
|
||||||
"body": body,
|
|
||||||
"prerelease": False,
|
|
||||||
"target_commitish": "master",
|
|
||||||
}
|
|
||||||
|
|
||||||
release_resp = None
|
|
||||||
for attempt in range(3):
|
|
||||||
try:
|
|
||||||
release_resp = requests.post(
|
|
||||||
release_api_uri, json=release_data, headers=auth_headers, timeout=30
|
|
||||||
)
|
|
||||||
release_resp.raise_for_status()
|
|
||||||
break
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
print(
|
|
||||||
f"Create release timed out, retrying in 30s... (attempt {attempt + 1})"
|
|
||||||
)
|
|
||||||
time.sleep(30)
|
|
||||||
except requests.exceptions.RequestException as err:
|
|
||||||
print(f"Create release failed: {err}")
|
|
||||||
if release_resp is not None:
|
|
||||||
print(f"Response: {release_resp.text}")
|
|
||||||
break
|
|
||||||
|
|
||||||
if release_resp is None or release_resp.status_code not in (200, 201):
|
|
||||||
print("Failed to create release on AtomGit, aborting.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"Created release {tag} on AtomGit")
|
|
||||||
|
|
||||||
for file_path in files:
|
|
||||||
upload_asset(access_token, tag, file_path)
|
|
||||||
|
|
||||||
print("Sync is completed!")
|
|
||||||
|
|
||||||
|
|
||||||
def upload_asset(access_token, tag, file_path):
|
|
||||||
file_name = os.path.basename(file_path)
|
|
||||||
upload_url_api = (
|
|
||||||
f"{ATOMGIT_API}/repos/{ATOMGIT_OWNER}/{ATOMGIT_REPO}"
|
|
||||||
f"/releases/{tag}/upload_url?file_name={file_name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
for attempt in range(3):
|
|
||||||
try:
|
|
||||||
resp = requests.get(
|
|
||||||
upload_url_api,
|
|
||||||
headers={"Authorization": f"Bearer {access_token}"},
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
upload_info = resp.json()
|
|
||||||
|
|
||||||
obs_url = upload_info["url"]
|
|
||||||
obs_headers = upload_info["headers"]
|
|
||||||
|
|
||||||
with open(file_path, "rb") as f:
|
|
||||||
put_resp = requests.put(
|
|
||||||
obs_url, headers=obs_headers, data=f, timeout=120
|
|
||||||
)
|
|
||||||
|
|
||||||
if put_resp.text.strip() == "success" or put_resp.status_code in (200, 201):
|
|
||||||
print(f"Uploaded {file_name}")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f"Upload {file_name} failed: {put_resp.status_code} {put_resp.text}"
|
|
||||||
)
|
|
||||||
except requests.exceptions.RequestException as err:
|
|
||||||
print(f"Upload {file_name} attempt {attempt + 1} failed: {err}")
|
|
||||||
time.sleep(10)
|
|
||||||
|
|
||||||
print(f"Failed to upload {file_name} after 3 attempts")
|
|
||||||
|
|
||||||
|
|
||||||
def get_abs_path(path):
|
|
||||||
return os.path.join(os.getcwd(), path)
|
|
||||||
|
|
||||||
|
|
||||||
get_github_latest_release()
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
# For most projects, this workflow file will not need changing; you simply need
|
|
||||||
# to commit it to your repository.
|
|
||||||
#
|
|
||||||
# You may wish to alter this file to override the set of languages analyzed,
|
|
||||||
# or to provide custom queries or build logic.
|
|
||||||
#
|
|
||||||
# ******** NOTE ********
|
|
||||||
# We have attempted to detect the languages in your repository. Please check
|
|
||||||
# the `language` matrix defined below to confirm you have the correct set of
|
|
||||||
# supported CodeQL languages.
|
|
||||||
#
|
|
||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ master ]
|
|
||||||
schedule:
|
|
||||||
- cron: '15 20 * * 0'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language: [ 'go' ]
|
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
|
||||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v3
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
|
||||||
|
|
||||||
# Generate swagger docs before build (cmd/dashboard/docs is .gitignored)
|
|
||||||
- name: Generate swagger docs
|
|
||||||
run: |
|
|
||||||
go install github.com/swaggo/swag/cmd/swag@latest
|
|
||||||
swag init --pd -d . -g ./cmd/dashboard/main.go -o ./cmd/dashboard/docs --requiredByDefault
|
|
||||||
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v3
|
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 https://git.io/JvXDl
|
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
|
||||||
# and modify them (or add more) to build your code if your project
|
|
||||||
# uses a compiled language
|
|
||||||
|
|
||||||
#- run: |
|
|
||||||
# make bootstrap
|
|
||||||
# make release
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v3
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
name: Contributors
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
contributors:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Generate Contributors Images
|
|
||||||
uses: jaywcjlove/github-action-contributors@main
|
|
||||||
id: contributors
|
|
||||||
with:
|
|
||||||
filter-author: (renovate\[bot\]|renovate-bot|dependabot\[bot\])
|
|
||||||
hideName: 'false' # Hide names in htmlTable
|
|
||||||
avatarSize: 50 # Set the avatar size.
|
|
||||||
truncate: 6
|
|
||||||
avatarMargin: 8
|
|
||||||
|
|
||||||
- name: Modify htmlTable README.md
|
|
||||||
uses: jaywcjlove/github-action-modify-file-content@main
|
|
||||||
with:
|
|
||||||
message: update contributors[no ci]
|
|
||||||
token: ${{ secrets.NAIBA_PAT }}
|
|
||||||
openDelimiter: '<!--GAMFC_DELIMITER-->'
|
|
||||||
closeDelimiter: '<!--GAMFC_DELIMITER_END-->'
|
|
||||||
path: README.md
|
|
||||||
body: '${{steps.contributors.outputs.htmlList}}'
|
|
||||||
@@ -7,6 +7,9 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
name: Sync Code to AtomGit
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync-code-to-atomgit:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: adambirds/sync-github-to-gitlab-action@v1.1.0
|
|
||||||
with:
|
|
||||||
destination_repository: git@atomgit.com:naiba/nezha-dashboard.git
|
|
||||||
destination_branch_name: master
|
|
||||||
destination_ssh_key: ${{ secrets.ATOMGIT_SSH_KEY }}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
name: Sync
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync-to-jihulab:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: adambirds/sync-github-to-gitlab-action@v1.1.0
|
|
||||||
with:
|
|
||||||
destination_repository: git@gitee.com:naibahq/nezha.git
|
|
||||||
destination_branch_name: master
|
|
||||||
destination_ssh_key: ${{ secrets.GITEE_SSH_KEY }}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
name: Sync Release to AtomGit
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync-release-to-atomgit:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 120
|
|
||||||
env:
|
|
||||||
ATOMGIT_PAT: ${{ secrets.ATOMGIT_PAT }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Sync to AtomGit
|
|
||||||
run: |
|
|
||||||
pip3 install PyGitHub
|
|
||||||
python3 .github/sync_atomgit.py
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
name: Sync Release to Gitee
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync-release-to-gitee:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 120
|
|
||||||
env:
|
|
||||||
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Sync to Gitee
|
|
||||||
run: |
|
|
||||||
pip3 install PyGitHub
|
|
||||||
python3 .github/sync.py
|
|
||||||
@@ -22,6 +22,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
env:
|
env:
|
||||||
GO111MODULE: on
|
GO111MODULE: on
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -30,8 +31,10 @@ jobs:
|
|||||||
go-version: "1.26.x"
|
go-version: "1.26.x"
|
||||||
|
|
||||||
- name: generate swagger docs
|
- name: generate swagger docs
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
go install github.com/swaggo/swag/cmd/swag@latest
|
go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
|
mkdir -p ./cmd/dashboard/user-dist ./cmd/dashboard/admin-dist
|
||||||
touch ./cmd/dashboard/user-dist/a
|
touch ./cmd/dashboard/user-dist/a
|
||||||
touch ./cmd/dashboard/admin-dist/a
|
touch ./cmd/dashboard/admin-dist/a
|
||||||
swag init --pd -d cmd/dashboard -g main.go -o cmd/dashboard/docs
|
swag init --pd -d cmd/dashboard -g main.go -o cmd/dashboard/docs
|
||||||
@@ -49,4 +52,4 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GOTOOLCHAIN: auto
|
GOTOOLCHAIN: auto
|
||||||
with:
|
with:
|
||||||
args: --exclude=G104,G115,G117,G203,G402,G703,G704 ./...
|
args: --exclude=G103,G104,G107,G115,G117,G203,G402,G703,G704 ./...
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ func routers(r *gin.Engine, frontendDist fs.FS) {
|
|||||||
auth.POST("/domains", commonHandler(AddDomain))
|
auth.POST("/domains", commonHandler(AddDomain))
|
||||||
auth.POST("/domains/:id/verify", commonHandler(VerifyDomain))
|
auth.POST("/domains/:id/verify", commonHandler(VerifyDomain))
|
||||||
auth.POST("/domains/:id/sync", commonHandler(SyncDomainWHOIS))
|
auth.POST("/domains/:id/sync", commonHandler(SyncDomainWHOIS))
|
||||||
|
auth.POST("/domains/sync-all", commonHandler(SyncAllDomains))
|
||||||
auth.PUT("/domains/:id", commonHandler(UpdateDomain))
|
auth.PUT("/domains/:id", commonHandler(UpdateDomain))
|
||||||
auth.DELETE("/domains/:id", commonHandler(DeleteDomain))
|
auth.DELETE("/domains/:id", commonHandler(DeleteDomain))
|
||||||
|
|
||||||
|
|||||||
@@ -137,3 +137,8 @@ func SyncDomainWHOIS(c *gin.Context) (any, error) {
|
|||||||
|
|
||||||
return domain, nil
|
return domain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SyncAllDomains(c *gin.Context) (any, error) {
|
||||||
|
singleton.SyncAllDomains()
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ func createNotification(c *gin.Context) (uint64, error) {
|
|||||||
n.RequestHeader = nf.RequestHeader
|
n.RequestHeader = nf.RequestHeader
|
||||||
n.RequestBody = nf.RequestBody
|
n.RequestBody = nf.RequestBody
|
||||||
n.URL = nf.URL
|
n.URL = nf.URL
|
||||||
|
n.Type = nf.Type
|
||||||
verifyTLS := nf.VerifyTLS
|
verifyTLS := nf.VerifyTLS
|
||||||
n.VerifyTLS = &verifyTLS
|
n.VerifyTLS = &verifyTLS
|
||||||
formatMetricUnits := nf.FormatMetricUnits
|
formatMetricUnits := nf.FormatMetricUnits
|
||||||
@@ -120,6 +121,7 @@ func updateNotification(c *gin.Context) (any, error) {
|
|||||||
n.RequestHeader = nf.RequestHeader
|
n.RequestHeader = nf.RequestHeader
|
||||||
n.RequestBody = nf.RequestBody
|
n.RequestBody = nf.RequestBody
|
||||||
n.URL = nf.URL
|
n.URL = nf.URL
|
||||||
|
n.Type = nf.Type
|
||||||
verifyTLS := nf.VerifyTLS
|
verifyTLS := nf.VerifyTLS
|
||||||
n.VerifyTLS = &verifyTLS
|
n.VerifyTLS = &verifyTLS
|
||||||
formatMetricUnits := nf.FormatMetricUnits
|
formatMetricUnits := nf.FormatMetricUnits
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ func updateConfig(c *gin.Context) (any, error) {
|
|||||||
singleton.Conf.CustomLogo = sf.CustomLogo
|
singleton.Conf.CustomLogo = sf.CustomLogo
|
||||||
singleton.Conf.CustomDescription = sf.CustomDescription
|
singleton.Conf.CustomDescription = sf.CustomDescription
|
||||||
singleton.Conf.CustomLinks = sf.CustomLinks
|
singleton.Conf.CustomLinks = sf.CustomLinks
|
||||||
singleton.Conf.BackgroundImageDay = sf.BackgroundImageDay
|
singleton.Conf.TelegramBotToken = sf.TelegramBotToken
|
||||||
singleton.Conf.BackgroundImageNight = sf.BackgroundImageNight
|
singleton.Conf.TelegramAdminChatID = sf.TelegramAdminChatID
|
||||||
|
|
||||||
if err := singleton.Conf.Save(); err != nil {
|
if err := singleton.Conf.Save(); err != nil {
|
||||||
return nil, newGormError("%v", err)
|
return nil, newGormError("%v", err)
|
||||||
|
|||||||
+3
-1
@@ -43,7 +43,6 @@ type ConfigDashboard struct {
|
|||||||
AdminTemplate string `koanf:"admin_template" json:"admin_template,omitempty"`
|
AdminTemplate string `koanf:"admin_template" json:"admin_template,omitempty"`
|
||||||
|
|
||||||
EnablePlainIPInNotification bool `koanf:"enable_plain_ip_in_notification" json:"enable_plain_ip_in_notification,omitempty"` // 通知信息IP不打码
|
EnablePlainIPInNotification bool `koanf:"enable_plain_ip_in_notification" json:"enable_plain_ip_in_notification,omitempty"` // 通知信息IP不打码
|
||||||
ExpiryNotificationGroupID uint64 `koanf:"expiry_notification_group_id" json:"expiry_notification_group_id"`
|
|
||||||
|
|
||||||
// IP变更提醒
|
// IP变更提醒
|
||||||
EnableIPChangeNotification bool `koanf:"enable_ip_change_notification" json:"enable_ip_change_notification,omitempty"`
|
EnableIPChangeNotification bool `koanf:"enable_ip_change_notification" json:"enable_ip_change_notification,omitempty"`
|
||||||
@@ -52,6 +51,9 @@ type ConfigDashboard struct {
|
|||||||
IgnoredIPNotification string `koanf:"ignored_ip_notification" json:"ignored_ip_notification,omitempty"` // 特定服务器IP(多个服务器用逗号分隔)
|
IgnoredIPNotification string `koanf:"ignored_ip_notification" json:"ignored_ip_notification,omitempty"` // 特定服务器IP(多个服务器用逗号分隔)
|
||||||
|
|
||||||
DNSServers string `koanf:"dns_servers" json:"dns_servers,omitempty"`
|
DNSServers string `koanf:"dns_servers" json:"dns_servers,omitempty"`
|
||||||
|
ExpiryNotificationGroupID uint64 `koanf:"expiry_notification_group_id" json:"expiry_notification_group_id,omitempty"`
|
||||||
|
TelegramBotToken string `koanf:"telegram_bot_token" json:"telegram_bot_token,omitempty"`
|
||||||
|
TelegramAdminChatID string `koanf:"telegram_admin_chat_id" json:"telegram_admin_chat_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|||||||
+127
-16
@@ -1,9 +1,12 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -176,9 +179,6 @@ func (ns *NotificationServerBundle) Send(message string) error {
|
|||||||
|
|
||||||
func (ns *NotificationServerBundle) sendSMTP(message string) error {
|
func (ns *NotificationServerBundle) sendSMTP(message string) error {
|
||||||
n := ns.Notification
|
n := ns.Notification
|
||||||
// RequestHeader: user:pass
|
|
||||||
// RequestBody: to_email
|
|
||||||
// URL: host:port
|
|
||||||
authInfo := strings.SplitN(n.RequestHeader, ":", 2)
|
authInfo := strings.SplitN(n.RequestHeader, ":", 2)
|
||||||
if len(authInfo) < 2 {
|
if len(authInfo) < 2 {
|
||||||
return errors.New("SMTP认证信息格式错误 (user:pass)")
|
return errors.New("SMTP认证信息格式错误 (user:pass)")
|
||||||
@@ -187,31 +187,129 @@ func (ns *NotificationServerBundle) sendSMTP(message string) error {
|
|||||||
pass := authInfo[1]
|
pass := authInfo[1]
|
||||||
to := n.RequestBody
|
to := n.RequestBody
|
||||||
|
|
||||||
hp := strings.SplitN(n.URL, ":", 2)
|
host, port, err := net.SplitHostPort(n.URL)
|
||||||
if len(hp) < 2 {
|
if err != nil {
|
||||||
return errors.New("SMTP服务器地址格式错误 (host:port)")
|
return errors.New("SMTP服务器地址格式错误 (host:port)")
|
||||||
}
|
}
|
||||||
|
|
||||||
auth := smtp.PlainAuth("", user, pass, hp[0])
|
|
||||||
|
|
||||||
subject := "Nezha Monitoring Alert"
|
subject := "Nezha Monitoring Alert"
|
||||||
if ns.Server != nil {
|
if ns.Server != nil {
|
||||||
subject = fmt.Sprintf("Nezha Alert: %s", ns.Server.Name)
|
subject = fmt.Sprintf("Nezha Alert: %s", ns.Server.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
body := fmt.Sprintf("To: %s\r\nSubject: %s\r\n\r\n%s", to, subject, message)
|
// 提取真实的发件人邮箱 (处理 username != email 的情况)
|
||||||
|
fromEmail := user
|
||||||
err := smtp.SendMail(n.URL, auth, user, []string{to}, []byte(body))
|
if !strings.Contains(user, "@") {
|
||||||
if err != nil {
|
// 如果用户名不是邮箱,为了防止被拦截,构造一个合法的From
|
||||||
return err
|
fromEmail = fmt.Sprintf("nezha@%s", host)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 遵循 RFC 2822
|
||||||
|
header := make(map[string]string)
|
||||||
|
header["From"] = fmt.Sprintf("Nezha Monitoring <%s>", fromEmail)
|
||||||
|
header["To"] = to
|
||||||
|
header["Subject"] = subject
|
||||||
|
header["Date"] = time.Now().Format(time.RFC1123Z)
|
||||||
|
header["Content-Type"] = "text/plain; charset=UTF-8"
|
||||||
|
|
||||||
|
var msg strings.Builder
|
||||||
|
for k, v := range header {
|
||||||
|
msg.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
|
||||||
|
}
|
||||||
|
msg.WriteString("\r\n")
|
||||||
|
msg.WriteString(message)
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: n.VerifyTLS == nil || !*n.VerifyTLS,
|
||||||
|
ServerName: host,
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := smtp.PlainAuth("", user, pass, host)
|
||||||
|
|
||||||
|
if port == "465" {
|
||||||
|
// SMTPS (Implicit SSL)
|
||||||
|
conn, err := tls.Dial("tcp", n.URL, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SMTP SSL Dial error: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client, err := smtp.NewClient(conn, host)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SMTP NewClient error: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Quit()
|
||||||
|
|
||||||
|
if err = client.Auth(auth); err != nil {
|
||||||
|
return fmt.Errorf("SMTP Auth error: %w", err)
|
||||||
|
}
|
||||||
|
if err = client.Mail(fromEmail); err != nil {
|
||||||
|
return fmt.Errorf("SMTP Mail error: %w", err)
|
||||||
|
}
|
||||||
|
if err = client.Rcpt(to); err != nil {
|
||||||
|
return fmt.Errorf("SMTP Rcpt error: %w", err)
|
||||||
|
}
|
||||||
|
w, err := client.Data()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SMTP Data error: %w", err)
|
||||||
|
}
|
||||||
|
_, err = w.Write([]byte(msg.String()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SMTP Write error: %w", err)
|
||||||
|
}
|
||||||
|
err = w.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SMTP Close error: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// STARTTLS (Port 25, 587, etc.)
|
||||||
|
conn, err := net.Dial("tcp", n.URL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SMTP Dial error: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client, err := smtp.NewClient(conn, host)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SMTP NewClient error: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Quit()
|
||||||
|
|
||||||
|
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||||
|
if err = client.StartTLS(tlsConfig); err != nil {
|
||||||
|
return fmt.Errorf("SMTP StartTLS error: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Auth(auth); err != nil {
|
||||||
|
return fmt.Errorf("SMTP Auth error: %w", err)
|
||||||
|
}
|
||||||
|
if err = client.Mail(fromEmail); err != nil {
|
||||||
|
return fmt.Errorf("SMTP Mail error: %w", err)
|
||||||
|
}
|
||||||
|
if err = client.Rcpt(to); err != nil {
|
||||||
|
return fmt.Errorf("SMTP Rcpt error: %w", err)
|
||||||
|
}
|
||||||
|
w, err := client.Data()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SMTP Data error: %w", err)
|
||||||
|
}
|
||||||
|
_, err = w.Write([]byte(msg.String()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SMTP Write error: %w", err)
|
||||||
|
}
|
||||||
|
err = w.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SMTP Close error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *NotificationServerBundle) sendTelegram(message string) error {
|
func (ns *NotificationServerBundle) sendTelegram(message string) error {
|
||||||
n := ns.Notification
|
n := ns.Notification
|
||||||
// URL: bot_token
|
|
||||||
// RequestHeader: chat_id
|
|
||||||
token := n.URL
|
token := n.URL
|
||||||
chatID := n.RequestHeader
|
chatID := n.RequestHeader
|
||||||
|
|
||||||
@@ -219,10 +317,23 @@ func (ns *NotificationServerBundle) sendTelegram(message string) error {
|
|||||||
|
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Add("chat_id", chatID)
|
params.Add("chat_id", chatID)
|
||||||
params.Add("text", message)
|
params.Add("text", html.EscapeString(message))
|
||||||
params.Add("parse_mode", "HTML")
|
params.Add("parse_mode", "HTML")
|
||||||
|
|
||||||
resp, err := http.PostForm(apiURL, params)
|
var client *http.Client
|
||||||
|
if n.VerifyTLS != nil && *n.VerifyTLS {
|
||||||
|
client = utils.HttpClient
|
||||||
|
} else {
|
||||||
|
client = utils.HttpClientSkipTlsVerify
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, apiURL, strings.NewReader(params.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ type NotificationForm struct {
|
|||||||
VerifyTLS bool `json:"verify_tls,omitempty" validate:"optional"`
|
VerifyTLS bool `json:"verify_tls,omitempty" validate:"optional"`
|
||||||
SkipCheck bool `json:"skip_check,omitempty" validate:"optional"`
|
SkipCheck bool `json:"skip_check,omitempty" validate:"optional"`
|
||||||
FormatMetricUnits bool `json:"format_metric_units,omitempty" validate:"optional"`
|
FormatMetricUnits bool `json:"format_metric_units,omitempty" validate:"optional"`
|
||||||
|
Type uint8 `json:"type,omitempty" validate:"optional"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ type SettingForm struct {
|
|||||||
AgentTLS bool `json:"tls,omitempty" validate:"optional"`
|
AgentTLS bool `json:"tls,omitempty" validate:"optional"`
|
||||||
EnableIPChangeNotification bool `json:"enable_ip_change_notification,omitempty" validate:"optional"`
|
EnableIPChangeNotification bool `json:"enable_ip_change_notification,omitempty" validate:"optional"`
|
||||||
EnablePlainIPInNotification bool `json:"enable_plain_ip_in_notification,omitempty" validate:"optional"`
|
EnablePlainIPInNotification bool `json:"enable_plain_ip_in_notification,omitempty" validate:"optional"`
|
||||||
ExpiryNotificationGroupID uint64 `json:"expiry_notification_group_id,omitempty" validate:"optional"`
|
ExpiryNotificationGroupID uint64 `json:"expiry_notification_group_id,omitempty"`
|
||||||
|
TelegramBotToken string `json:"telegram_bot_token,omitempty" validate:"optional"`
|
||||||
|
TelegramAdminChatID string `json:"telegram_admin_chat_id,omitempty" validate:"optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Setting struct {
|
type Setting struct {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ type testSt struct {
|
|||||||
func TestSplitDomainSOA(t *testing.T) {
|
func TestSplitDomainSOA(t *testing.T) {
|
||||||
cases := []testSt{
|
cases := []testSt{
|
||||||
{
|
{
|
||||||
domain: "www.example.co.uk",
|
domain: "www.google.co.uk",
|
||||||
zone: "example.co.uk.",
|
zone: "google.co.uk.",
|
||||||
prefix: "www",
|
prefix: "www",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -195,6 +195,32 @@ func SyncDomainWHOIS(d *model.Domain) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SyncAllDomains 异步批量同步所有已验证域名的 Whois 和价格信息
|
||||||
|
func SyncAllDomains() {
|
||||||
|
go func() {
|
||||||
|
log.Println("NEZHA>> 开始批量同步所有域名的 Whois 和价格信息...")
|
||||||
|
domains, err := GetDomains("admin")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("NEZHA>> 批量同步域名失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
successCount := 0
|
||||||
|
for _, d := range domains {
|
||||||
|
if d.Status == "verified" {
|
||||||
|
if err := SyncDomainWHOIS(&d); err != nil {
|
||||||
|
log.Printf("NEZHA>> 域名 %s 同步失败: %v", d.Domain, err)
|
||||||
|
} else {
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
// 避免并发过高被 API 限制
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("NEZHA>> 批量同步域名结束,成功 %d/%d", successCount, len(domains))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
// GetDomains 获取所有域名记录
|
// GetDomains 获取所有域名记录
|
||||||
func GetDomains(scope string) ([]model.Domain, error) {
|
func GetDomains(scope string) ([]model.Domain, error) {
|
||||||
var domains []model.Domain
|
var domains []model.Domain
|
||||||
@@ -354,7 +380,7 @@ func CronJobForDomainStatus() {
|
|||||||
msg := ""
|
msg := ""
|
||||||
switch daysLeft + 1 {
|
switch daysLeft + 1 {
|
||||||
case 60, 30, 15, 7, 3, 1:
|
case 60, 30, 15, 7, 3, 1:
|
||||||
msg = fmt.Sprintf("域名 [%s] 即将到期,剩余 %d 天。到期时间: %s", d.Domain, daysLeft+1, endDate.Format("2006-01-02"))
|
msg = fmt.Sprintf("域名 [%s] 即通知期,剩余 %d 天。到期时间: %s", d.Domain, daysLeft+1, endDate.Format("2006-01-02"))
|
||||||
case 0:
|
case 0:
|
||||||
msg = fmt.Sprintf("域名 [%s] 已到期!到期时间: %s", d.Domain, endDate.Format("2006-01-02"))
|
msg = fmt.Sprintf("域名 [%s] 已到期!到期时间: %s", d.Domain, endDate.Format("2006-01-02"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ func LoadSingleton(bus chan<- *model.Service) (err error) {
|
|||||||
CronShared = NewCronClass()
|
CronShared = NewCronClass()
|
||||||
// 最后初始化 ServiceSentinel
|
// 最后初始化 ServiceSentinel
|
||||||
ServiceSentinelShared, err = NewServiceSentinel(bus)
|
ServiceSentinelShared, err = NewServiceSentinel(bus)
|
||||||
|
if err == nil {
|
||||||
|
InitTelegramBot()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
package singleton
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nezhahq/nezha/model"
|
||||||
|
"github.com/nezhahq/nezha/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tgUpdate struct {
|
||||||
|
UpdateID int `json:"update_id"`
|
||||||
|
Message *struct {
|
||||||
|
MessageID int `json:"message_id"`
|
||||||
|
From *struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
} `json:"from"`
|
||||||
|
Chat *struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
} `json:"chat"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
} `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitTelegramBot() {
|
||||||
|
if Conf.TelegramBotToken == "" {
|
||||||
|
log.Println("NEZHA>> TG Bot Token 未配置,跳过启动互动机器人")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("NEZHA>> 正在启动 Telegram 互动机器人...")
|
||||||
|
go func() {
|
||||||
|
offset := 0
|
||||||
|
for {
|
||||||
|
updates, err := getTGUpdates(Conf.TelegramBotToken, offset)
|
||||||
|
if err != nil {
|
||||||
|
// 避免过于频繁报错
|
||||||
|
time.Sleep(30 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, update := range updates {
|
||||||
|
offset = update.UpdateID + 1
|
||||||
|
if update.Message != nil {
|
||||||
|
handleTGUpdate(update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTGUpdates(token string, offset int) ([]tgUpdate, error) {
|
||||||
|
apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/getUpdates?offset=%d&timeout=20", token, offset)
|
||||||
|
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp, err := utils.HttpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Result []tgUpdate `json:"result"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result.Result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTGUpdate(update tgUpdate) {
|
||||||
|
if update.Message == nil || update.Message.Chat == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chatID := update.Message.Chat.ID
|
||||||
|
adminChatID, _ := strconv.ParseInt(Conf.TelegramAdminChatID, 10, 64)
|
||||||
|
|
||||||
|
// 权限检查
|
||||||
|
if adminChatID != 0 && chatID != adminChatID {
|
||||||
|
sendTGMessage(chatID, "🚫 您没有权限操作此机器人。")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
text := update.Message.Text
|
||||||
|
switch {
|
||||||
|
case text == "/start" || text == "/help":
|
||||||
|
sendTGMainMenu(chatID)
|
||||||
|
case text == "/status" || text == "📊 运行状态":
|
||||||
|
sendTGStatus(chatID)
|
||||||
|
case text == "/domains" || text == "🌐 域名监控":
|
||||||
|
sendTGDomains(chatID)
|
||||||
|
default:
|
||||||
|
if strings.HasPrefix(text, "/") {
|
||||||
|
sendTGMessage(chatID, "❓ 未知命令,请输入 /start 查看菜单。")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendTGMainMenu(chatID int64) {
|
||||||
|
menu := "👋 您好!我是哪吒监控助手。\n\n请选择以下操作:"
|
||||||
|
keyboard := map[string]interface{}{
|
||||||
|
"keyboard": [][]map[string]string{
|
||||||
|
{{"text": "📊 运行状态"}, {"text": "🌐 域名监控"}},
|
||||||
|
},
|
||||||
|
"resize_keyboard": true,
|
||||||
|
}
|
||||||
|
kbJSON, _ := json.Marshal(keyboard)
|
||||||
|
sendTGRequest("sendMessage", url.Values{
|
||||||
|
"chat_id": {strconv.FormatInt(chatID, 10)},
|
||||||
|
"text": {menu},
|
||||||
|
"reply_markup": {string(kbJSON)},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendTGStatus(chatID int64) {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("📊 <b>服务器实时状态</b>\n\n")
|
||||||
|
|
||||||
|
ServerShared.Range(func(id uint64, s *model.Server) bool {
|
||||||
|
statusIcon := "🟢"
|
||||||
|
if !s.LastActive.After(time.Now().Add(-time.Second * 30)) {
|
||||||
|
statusIcon = "🔴"
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("%s <b>%s</b>\n", statusIcon, s.Name))
|
||||||
|
sb.WriteString(fmt.Sprintf("├ CPU: %.1f%% | Mem: %.1f%%\n", s.State.CPU, float64(s.State.MemUsed)/float64(s.Host.MemTotal)*100))
|
||||||
|
sb.WriteString(fmt.Sprintf("└ Net: ↓%s/s ↑%s/s\n\n", utils.Bytes(s.State.NetInSpeed), utils.Bytes(s.State.NetOutSpeed)))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if sb.Len() < 50 {
|
||||||
|
sb.WriteString("暂无在线服务器。")
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTGMessage(chatID, sb.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendTGDomains(chatID int64) {
|
||||||
|
domains, err := GetDomains("admin")
|
||||||
|
if err != nil {
|
||||||
|
sendTGMessage(chatID, "❌ 获取域名列表失败。")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("🌐 <b>域名监控状态</b>\n\n")
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for _, d := range domains {
|
||||||
|
statusIcon := "✅"
|
||||||
|
if d.Status == "pending" {
|
||||||
|
statusIcon = "⏳"
|
||||||
|
} else if d.Status == "expired" {
|
||||||
|
statusIcon = "❌"
|
||||||
|
}
|
||||||
|
|
||||||
|
expiresInfo := "N/A"
|
||||||
|
if d.BillingData != nil {
|
||||||
|
var billing model.BillingDataMod
|
||||||
|
if json.Unmarshal(d.BillingData, &billing) == nil && billing.EndDate != "" {
|
||||||
|
if endDate, err := time.Parse(time.RFC3339, billing.EndDate); err == nil {
|
||||||
|
daysLeft := int(endDate.Sub(now).Hours() / 24)
|
||||||
|
expiresInfo = fmt.Sprintf("%d 天", daysLeft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString(fmt.Sprintf("%s <b>%s</b>\n", statusIcon, d.Domain))
|
||||||
|
sb.WriteString(fmt.Sprintf("└ 剩余: %s | 状态: %s\n\n", expiresInfo, d.Status))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(domains) == 0 {
|
||||||
|
sb.WriteString("暂无监控中的域名。")
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTGMessage(chatID, sb.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendTGMessage(chatID int64, text string) {
|
||||||
|
sendTGRequest("sendMessage", url.Values{
|
||||||
|
"chat_id": {strconv.FormatInt(chatID, 10)},
|
||||||
|
"text": {text},
|
||||||
|
"parse_mode": {"HTML"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendTGRequest(method string, params url.Values) {
|
||||||
|
apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/%s", Conf.TelegramBotToken, method)
|
||||||
|
req, err := http.NewRequest(http.MethodPost, apiURL, strings.NewReader(params.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
resp, err := utils.HttpClient.Do(req)
|
||||||
|
if err == nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &dns.Client{Timeout: 10 * time.Second}
|
||||||
|
domain := "example.co.uk."
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(domain, dns.TypeSOA)
|
||||||
|
|
||||||
|
r, _, err := c.Exchange(m, "1.1.1.1:53")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Answer count: %d\n", len(r.Answer))
|
||||||
|
for _, a := range r.Answer {
|
||||||
|
fmt.Printf("Answer: %v\n", a)
|
||||||
|
}
|
||||||
|
fmt.Printf("Ns count: %d\n", len(r.Ns))
|
||||||
|
for _, a := range r.Ns {
|
||||||
|
fmt.Printf("Ns: %v\n", a)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user