python转成go测试

This commit is contained in:
wood chen 2024-09-18 01:27:32 +08:00
parent 8c265e8529
commit 3697051ecf
15 changed files with 1112 additions and 23 deletions

View File

@ -5,7 +5,7 @@
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View File

@ -18,15 +18,16 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
- name: Set up Go
uses: actions/setup-go@v4
with:
python-version: '3.12.5'
go-version: '1.22' # 使用你项目需要的 Go 版本
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Build for amd64
run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main-amd64 .
- name: Build for arm64
run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o main-arm64 .
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@ -44,7 +45,7 @@ jobs:
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
file: Dockerfile.multi
platforms: linux/amd64,linux/arm64
push: true
tags: |

View File

@ -1,15 +1,43 @@
FROM python:3.12.5-slim
# 设置时区
ENV TZ=Asia/Singapore
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 使用官方 Go 镜像作为构建环境
FROM golang:1.22 AS builder
# 设置工作目录
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制 go mod 和 sum 文件
COPY go.mod go.sum ./
COPY src /app/src
COPY data /app/data
# 下载依赖
RUN go mod download
CMD ["python", "src/main.py"]
# 复制源代码
COPY . .
# 编译应用
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# 使用轻量级的 alpine 镜像作为运行环境
FROM alpine:latest
# 安装 ca-certificates
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# 从构建阶段复制编译好的应用
COPY --from=builder /app/main .
# 设置环境变量
ENV BOT_TOKEN=""
ENV ADMIN_ID=""
ENV SYMBOLS=""
ENV DEBUG_MODE="false"
# 创建数据目录
RUN mkdir -p /app/data
# 暴露端口(如果你的应用需要的话)
# EXPOSE 8080
# 运行应用
CMD ["./main"]

25
Dockerfile.multi Normal file
View File

@ -0,0 +1,25 @@
# 使用轻量级的基础镜像
FROM alpine:latest
# 安装 ca-certificates通常需要用于 HTTPS
RUN apk --no-cache add ca-certificates
# 创建工作目录
WORKDIR /root/
# 复制编译好的可执行文件
COPY main-amd64 main-arm64 ./
# 使用 TARGETARCH 参数来选择正确的二进制文件
ARG TARGETARCH
RUN if [ "$TARGETARCH" = "amd64" ]; then \
mv main-amd64 main && rm main-arm64; \
elif [ "$TARGETARCH" = "arm64" ]; then \
mv main-arm64 main && rm main-amd64; \
fi
# 设置执行权限
RUN chmod +x main
# 运行应用
CMD ["./main"]

34
core/bot_commands.go Normal file
View File

@ -0,0 +1,34 @@
package core
import (
"fmt"
"log"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
func RegisterCommands(bot *tgbotapi.BotAPI, adminID int64) error {
commands := []tgbotapi.BotCommand{
{Command: "add", Description: "添加新的关键词"},
{Command: "delete", Description: "删除现有的关键词"},
{Command: "list", Description: "列出所有当前的关键词"},
{Command: "deletecontaining", Description: "删除所有包含指定词语的关键词"},
{Command: "addwhite", Description: "添加域名到白名单"},
{Command: "delwhite", Description: "从白名单移除域名"},
{Command: "listwhite", Description: "列出白名单域名"},
}
scope := tgbotapi.NewBotCommandScopeChatAdministrators(adminID)
config := tgbotapi.NewSetMyCommands(commands...)
config.Scope = &scope // 注意这里使用 &scope 来获取指针
config.LanguageCode = "" // 空字符串表示默认语言
_, err := bot.Request(config)
if err != nil {
return fmt.Errorf("failed to register bot commands: %w", err)
}
log.Println("Bot commands registered successfully.")
return nil
}

184
core/database.go Normal file
View File

@ -0,0 +1,184 @@
package core
import (
"database/sql"
"os"
"path/filepath"
"sync"
"time"
_ "github.com/mattn/go-sqlite3"
)
type Database struct {
db *sql.DB
dbFile string
keywordsCache []string
whitelistCache []string
cacheTime time.Time
mu sync.Mutex
}
func NewDatabase(dbFile string) (*Database, error) {
os.MkdirAll(filepath.Dir(dbFile), os.ModePerm)
db, err := sql.Open("sqlite3", dbFile)
if err != nil {
return nil, err
}
database := &Database{
db: db,
dbFile: dbFile,
}
if err := database.createTables(); err != nil {
return nil, err
}
return database, nil
}
func (d *Database) createTables() error {
queries := []string{
`CREATE TABLE IF NOT EXISTS keywords
(id INTEGER PRIMARY KEY, keyword TEXT UNIQUE)`,
`CREATE INDEX IF NOT EXISTS idx_keyword ON keywords(keyword)`,
`CREATE TABLE IF NOT EXISTS whitelist
(id INTEGER PRIMARY KEY, domain TEXT UNIQUE)`,
`CREATE INDEX IF NOT EXISTS idx_domain ON whitelist(domain)`,
`CREATE VIRTUAL TABLE IF NOT EXISTS keywords_fts USING fts5(keyword)`,
}
for _, query := range queries {
_, err := d.db.Exec(query)
if err != nil {
return err
}
}
return nil
}
func (d *Database) executeQuery(query string, args ...interface{}) ([]string, error) {
rows, err := d.db.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var results []string
for rows.Next() {
var result string
if err := rows.Scan(&result); err != nil {
return nil, err
}
results = append(results, result)
}
return results, nil
}
func (d *Database) AddKeyword(keyword string) error {
_, err := d.db.Exec("INSERT OR IGNORE INTO keywords (keyword) VALUES (?)", keyword)
if err != nil {
return err
}
_, err = d.db.Exec("INSERT OR IGNORE INTO keywords_fts (keyword) VALUES (?)", keyword)
if err != nil {
return err
}
d.invalidateCache()
return nil
}
func (d *Database) RemoveKeyword(keyword string) error {
_, err := d.db.Exec("DELETE FROM keywords WHERE keyword = ?", keyword)
if err != nil {
return err
}
_, err = d.db.Exec("DELETE FROM keywords_fts WHERE keyword = ?", keyword)
if err != nil {
return err
}
d.invalidateCache()
return nil
}
func (d *Database) GetAllKeywords() ([]string, error) {
d.mu.Lock()
defer d.mu.Unlock()
if d.keywordsCache == nil || time.Since(d.cacheTime) > 5*time.Minute {
keywords, err := d.executeQuery("SELECT keyword FROM keywords")
if err != nil {
return nil, err
}
d.keywordsCache = keywords
d.cacheTime = time.Now()
}
return d.keywordsCache, nil
}
func (d *Database) RemoveKeywordsContaining(substring string) error {
_, err := d.db.Exec("DELETE FROM keywords WHERE keyword LIKE ?", "%"+substring+"%")
if err != nil {
return err
}
_, err = d.db.Exec("DELETE FROM keywords_fts WHERE keyword LIKE ?", "%"+substring+"%")
if err != nil {
return err
}
d.invalidateCache()
return nil
}
func (d *Database) AddWhitelist(domain string) error {
_, err := d.db.Exec("INSERT OR IGNORE INTO whitelist (domain) VALUES (?)", domain)
if err != nil {
return err
}
d.invalidateCache()
return nil
}
func (d *Database) RemoveWhitelist(domain string) error {
_, err := d.db.Exec("DELETE FROM whitelist WHERE domain = ?", domain)
if err != nil {
return err
}
d.invalidateCache()
return nil
}
func (d *Database) GetAllWhitelist() ([]string, error) {
d.mu.Lock()
defer d.mu.Unlock()
if d.whitelistCache == nil || time.Since(d.cacheTime) > 5*time.Minute {
whitelist, err := d.executeQuery("SELECT domain FROM whitelist")
if err != nil {
return nil, err
}
d.whitelistCache = whitelist
d.cacheTime = time.Now()
}
return d.whitelistCache, nil
}
func (d *Database) SearchKeywords(pattern string) ([]string, error) {
return d.executeQuery("SELECT keyword FROM keywords_fts WHERE keyword MATCH ?", pattern)
}
func (d *Database) invalidateCache() {
d.mu.Lock()
defer d.mu.Unlock()
d.keywordsCache = nil
d.whitelistCache = nil
d.cacheTime = time.Time{}
}
func (d *Database) Close() error {
return d.db.Close()
}

90
core/functions.go Normal file
View File

@ -0,0 +1,90 @@
package core
import (
"fmt"
"strings"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
// SendLongMessage sends a long message by splitting it into multiple messages if necessary
// SendLongMessage sends a long message by splitting it into multiple messages if necessary
func SendLongMessage(bot *tgbotapi.BotAPI, chatID int64, prefix string, items []string) error {
const maxMessageLength = 4000 // Leave some room for Telegram's message limit
message := prefix + "\n"
for i, item := range items {
newLine := fmt.Sprintf("%d. %s\n", i+1, item)
if len(message)+len(newLine) > maxMessageLength {
msg := tgbotapi.NewMessage(chatID, message)
_, err := bot.Send(msg)
if err != nil {
return err
}
message = ""
}
message += newLine
}
if message != "" {
msg := tgbotapi.NewMessage(chatID, message)
_, err := bot.Send(msg)
if err != nil {
return err
}
}
return nil
}
// SendLongMessageWithoutNumbering sends a long message without numbering the items
func SendLongMessageWithoutNumbering(bot *tgbotapi.BotAPI, chatID int64, prefix string, items []string) error {
const maxMessageLength = 4000 // Leave some room for Telegram's message limit
message := prefix + "\n"
for _, item := range items {
newLine := item + "\n"
if len(message)+len(newLine) > maxMessageLength {
msg := tgbotapi.NewMessage(chatID, message)
_, err := bot.Send(msg)
if err != nil {
return err
}
message = ""
}
message += newLine
}
if message != "" {
msg := tgbotapi.NewMessage(chatID, message)
_, err := bot.Send(msg)
if err != nil {
return err
}
}
return nil
}
// JoinLongMessage joins items into a single long message, splitting it if necessary
func JoinLongMessage(prefix string, items []string) []string {
const maxMessageLength = 4000 // Leave some room for Telegram's message limit
var messages []string
message := prefix + "\n"
for i, item := range items {
newLine := fmt.Sprintf("%d. %s\n", i+1, item)
if len(message)+len(newLine) > maxMessageLength {
messages = append(messages, strings.TrimSpace(message))
message = ""
}
message += newLine
}
if message != "" {
messages = append(messages, strings.TrimSpace(message))
}
return messages
}

227
core/link_filter.go Normal file
View File

@ -0,0 +1,227 @@
package core
import (
"fmt"
"log"
"net/url"
"regexp"
"strings"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
var logger = log.New(log.Writer(), "LinkFilter: ", log.Ldate|log.Ltime|log.Lshortfile)
type LinkFilter struct {
db *Database
keywords []string
whitelist []string
linkPattern *regexp.Regexp
}
func NewLinkFilter(dbFile string) *LinkFilter {
lf := &LinkFilter{
db: NewDatabase(dbFile),
}
lf.linkPattern = regexp.MustCompile(`(?i)\b(?:(?:https?://)?(?:(?:www\.)?(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(?:t\.me|telegram\.me))(?:/[^\s]*)?)`)
lf.LoadDataFromFile()
return lf
}
func (lf *LinkFilter) LoadDataFromFile() {
lf.keywords = lf.db.GetAllKeywords()
lf.whitelist = lf.db.GetAllWhitelist()
logger.Printf("Loaded %d keywords and %d whitelist entries from database", len(lf.keywords), len(lf.whitelist))
}
func (lf *LinkFilter) NormalizeLink(link string) string {
link = regexp.MustCompile(`^https?://`).ReplaceAllString(link, "")
link = strings.TrimPrefix(link, "/")
parsedURL, err := url.Parse("http://" + link)
if err != nil {
logger.Printf("Error parsing URL: %v", err)
return link
}
normalized := fmt.Sprintf("%s%s", parsedURL.Hostname(), parsedURL.EscapedPath())
result := strings.TrimSuffix(normalized, "/")
logger.Printf("Normalized link: %s -> %s", link, result)
return result
}
func (lf *LinkFilter) ExtractDomain(urlStr string) string {
parsedURL, err := url.Parse(urlStr)
if err != nil {
logger.Printf("Error parsing URL: %v", err)
return urlStr
}
domain := parsedURL.Hostname()
parts := strings.Split(domain, ".")
if len(parts) > 2 {
domain = strings.Join(parts[len(parts)-2:], ".")
}
return strings.ToLower(domain)
}
func (lf *LinkFilter) IsWhitelisted(link string) bool {
domain := lf.ExtractDomain(link)
for _, whiteDomain := range lf.whitelist {
if domain == whiteDomain {
logger.Printf("Whitelist check for %s: Passed", link)
return true
}
}
logger.Printf("Whitelist check for %s: Failed", link)
return false
}
func (lf *LinkFilter) AddKeyword(keyword string) {
if lf.linkPattern.MatchString(keyword) {
keyword = lf.NormalizeLink(keyword)
}
keyword = strings.TrimPrefix(keyword, "/")
for _, k := range lf.keywords {
if k == keyword {
logger.Printf("Keyword already exists: %s", keyword)
return
}
}
lf.db.AddKeyword(keyword)
logger.Printf("New keyword added: %s", keyword)
lf.LoadDataFromFile()
}
func (lf *LinkFilter) RemoveKeyword(keyword string) bool {
for _, k := range lf.keywords {
if k == keyword {
lf.db.RemoveKeyword(keyword)
lf.LoadDataFromFile()
return true
}
}
return false
}
func (lf *LinkFilter) RemoveKeywordsContaining(substring string) []string {
removed := lf.db.RemoveKeywordsContaining(substring)
lf.LoadDataFromFile()
return removed
}
func (lf *LinkFilter) ShouldFilter(text string) (bool, []string) {
logger.Printf("Checking text: %s", text)
for _, keyword := range lf.keywords {
if strings.Contains(strings.ToLower(text), strings.ToLower(keyword)) {
logger.Printf("Text contains keyword: %s", text)
return true, nil
}
}
links := lf.linkPattern.FindAllString(text, -1)
logger.Printf("Found links: %v", links)
var newNonWhitelistedLinks []string
for _, link := range links {
normalizedLink := lf.NormalizeLink(link)
normalizedLink = strings.TrimPrefix(normalizedLink, "/")
if !lf.IsWhitelisted(normalizedLink) {
logger.Printf("Link not whitelisted: %s", normalizedLink)
found := false
for _, keyword := range lf.keywords {
if keyword == normalizedLink {
logger.Printf("Existing keyword found: %s", normalizedLink)
return true, nil
}
}
if !found {
newNonWhitelistedLinks = append(newNonWhitelistedLinks, normalizedLink)
lf.AddKeyword(normalizedLink)
}
}
}
if len(newNonWhitelistedLinks) > 0 {
logger.Printf("New non-whitelisted links found: %v", newNonWhitelistedLinks)
}
return false, newNonWhitelistedLinks
}
func (lf *LinkFilter) HandleKeywordCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message, command string, args string) {
switch command {
case "list":
keywords := lf.db.GetAllKeywords()
if len(keywords) == 0 {
bot.Send(tgbotapi.NewMessage(message.Chat.ID, "关键词列表为空。"))
} else {
SendLongMessage(bot, message.Chat.ID, "当前关键词列表:", keywords)
}
case "add":
if args != "" {
keyword := args
if !lf.db.KeywordExists(keyword) {
lf.AddKeyword(keyword)
bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("关键词 '%s' 已添加。", keyword)))
} else {
bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("关键词 '%s' 已存在。", keyword)))
}
}
case "delete":
if args != "" {
keyword := args
if lf.RemoveKeyword(keyword) {
bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("关键词 '%s' 已删除。", keyword)))
} else {
similarKeywords := lf.db.SearchKeywords(keyword)
if len(similarKeywords) > 0 {
SendLongMessage(bot, message.Chat.ID, fmt.Sprintf("未找到精确匹配的关键词 '%s'。\n\n以下是相似的关键词", keyword), similarKeywords)
} else {
bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("关键词 '%s' 不存在。", keyword)))
}
}
}
case "deletecontaining":
if args != "" {
substring := args
removedKeywords := lf.RemoveKeywordsContaining(substring)
if len(removedKeywords) > 0 {
SendLongMessage(bot, message.Chat.ID, fmt.Sprintf("已删除包含 '%s' 的以下关键词:", substring), removedKeywords)
} else {
bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("没有找到包含 '%s' 的关键词。", substring)))
}
}
default:
bot.Send(tgbotapi.NewMessage(message.Chat.ID, "无效的命令或参数。"))
}
}
func (lf *LinkFilter) HandleWhitelistCommand(bot *tgbotapi.BotAPI, message *tgbotapi.Message, command string, args string) {
switch command {
case "listwhite":
whitelist := lf.db.GetAllWhitelist()
if len(whitelist) == 0 {
bot.Send(tgbotapi.NewMessage(message.Chat.ID, "白名单为空。"))
} else {
SendLongMessageWithoutNumbering(bot, message.Chat.ID, "白名单域名列表:", whitelist)
}
case "addwhite":
if args != "" {
domain := strings.ToLower(args)
if !lf.db.WhitelistExists(domain) {
lf.db.AddWhitelist(domain)
bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("域名 '%s' 已添加到白名单。", domain)))
} else {
bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("域名 '%s' 已在白名单中。", domain)))
}
}
case "delwhite":
if args != "" {
domain := strings.ToLower(args)
if lf.db.WhitelistExists(domain) {
lf.db.RemoveWhitelist(domain)
bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("域名 '%s' 已从白名单中删除。", domain)))
} else {
bot.Send(tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("域名 '%s' 不在白名单中。", domain)))
}
}
default:
bot.Send(tgbotapi.NewMessage(message.Chat.ID, "无效的命令或参数。"))
}
}

View File

@ -1 +0,0 @@
["推广", "广告", "ad", "promotion"]

View File

@ -1,3 +0,0 @@
[
"q58.org"
]

18
go.mod Normal file
View File

@ -0,0 +1,18 @@
module github.com/woodchen-ink/Q58Bot
go 1.21.3
require (
github.com/adshao/go-binance/v2 v2.6.0
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/mattn/go-sqlite3 v1.14.23
)
require (
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
)

37
go.sum Normal file
View File

@ -0,0 +1,37 @@
github.com/adshao/go-binance/v2 v2.6.0 h1:sXPkfix+SgBojJmkt+sNJbJBQZOJK5GFP/WtAu+B5r0=
github.com/adshao/go-binance/v2 v2.6.0/go.mod h1:41Up2dG4NfMXpCldrDPETEtiOq+pHoGsFZ73xGgaumo=
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

107
main.go Normal file
View File

@ -0,0 +1,107 @@
package main
import (
"log"
"os"
"strconv"
"time"
binance "github.com/woodchen-ink/Q58Bot/service"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
var (
BOT_TOKEN string
ADMIN_ID int64
)
func init() {
BOT_TOKEN = os.Getenv("BOT_TOKEN")
adminIDStr := os.Getenv("ADMIN_ID")
var err error
ADMIN_ID, err = strconv.ParseInt(adminIDStr, 10, 64)
if err != nil {
log.Fatalf("Invalid ADMIN_ID: %v", err)
}
}
func setupBot() {
bot, err := tgbotapi.NewBotAPI(BOT_TOKEN)
if err != nil {
log.Panic(err)
}
bot.Debug = true
log.Printf("Authorized on account %s", bot.Self.UserName)
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil {
continue
}
if update.Message.Chat.ID != ADMIN_ID {
continue
}
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "")
switch update.Message.Command() {
case "start":
msg.Text = "Hello! I'm your bot."
case "help":
msg.Text = "I can help you with various tasks."
default:
msg.Text = "I don't know that command"
}
if _, err := bot.Send(msg); err != nil {
log.Panic(err)
}
}
}
func runGuard() {
for {
try(func() {
guard.Run()
}, "Guard")
}
}
func runBinance() {
for {
try(func() {
binance.Run()
}, "Binance")
}
}
func try(fn func(), name string) {
defer func() {
if r := recover(); r != nil {
log.Printf("%s process crashed: %v", name, r)
log.Printf("Restarting %s process...", name)
time.Sleep(time.Second) // 添加短暂延迟以防止过快重启
}
}()
fn()
}
func main() {
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
// 使用 goroutines 运行 bot、guard 和 binance 服务
go setupBot()
go runGuard()
go runBinance()
// 保持主程序运行
select {}
}

149
service/binance.go Normal file
View File

@ -0,0 +1,149 @@
package service
import (
"fmt"
"log"
"os"
"strconv"
"strings"
"time"
"github.com/adshao/go-binance/v2"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
var (
botToken string
chatID int64
symbols []string
bot *tgbotapi.BotAPI
lastMsgID int
singaporeTZ *time.Location
)
func init() {
var err error
botToken = os.Getenv("BOT_TOKEN")
chatID = mustParseInt64(os.Getenv("CHAT_ID"))
symbols = strings.Split(os.Getenv("SYMBOLS"), ",")
singaporeTZ, err = time.LoadLocation("Asia/Singapore")
if err != nil {
log.Fatal(err)
}
bot, err = tgbotapi.NewBotAPI(botToken)
if err != nil {
log.Fatal(err)
}
}
func mustParseInt64(s string) int64 {
i, err := strconv.ParseInt(s, 10, 64)
if err != nil {
log.Fatal(err)
}
return i
}
type tickerInfo struct {
symbol string
last float64
changePercent float64
}
func getTickerInfo(symbol string) (tickerInfo, error) {
client := binance.NewClient("", "")
// 获取当前价格
ticker, err := client.NewListPricesService().Symbol(symbol).Do(binance.NewContext())
if err != nil {
return tickerInfo{}, err
}
if len(ticker) == 0 {
return tickerInfo{}, fmt.Errorf("no ticker found for symbol %s", symbol)
}
last, err := ticker[0].Price.Float64()
if err != nil {
return tickerInfo{}, err
}
// 获取24小时价格变化
stats, err := client.NewListPriceChangeStatsService().Symbol(symbol).Do(binance.NewContext())
if err != nil {
return tickerInfo{}, err
}
if len(stats) == 0 {
return tickerInfo{}, fmt.Errorf("no price change stats found for symbol %s", symbol)
}
changePercent, err := stats[0].PriceChangePercent.Float64()
if err != nil {
return tickerInfo{}, err
}
return tickerInfo{
symbol: symbol,
last: last,
changePercent: changePercent,
}, nil
}
func formatChange(changePercent float64) string {
if changePercent > 0 {
return fmt.Sprintf("🔼 +%.2f%%", changePercent)
} else if changePercent < 0 {
return fmt.Sprintf("🔽 %.2f%%", changePercent)
}
return fmt.Sprintf("◀▶ %.2f%%", changePercent)
}
func sendPriceUpdate() {
now := time.Now().In(singaporeTZ)
message := fmt.Sprintf("市场更新 - %s (SGT)\n\n", now.Format("2006-01-02 15:04:05"))
for _, symbol := range symbols {
info, err := getTickerInfo(symbol)
if err != nil {
log.Printf("Error getting ticker info for %s: %v", symbol, err)
continue
}
changeStr := formatChange(info.changePercent)
message += fmt.Sprintf("*%s*\n", info.symbol)
message += fmt.Sprintf("价格: $%.7f\n", info.last)
message += fmt.Sprintf("24h 涨跌: %s\n\n", changeStr)
}
if lastMsgID != 0 {
deleteMsg := tgbotapi.NewDeleteMessage(chatID, lastMsgID)
_, err := bot.Request(deleteMsg)
if err != nil {
log.Printf("Failed to delete previous message: %v", err)
}
}
msg := tgbotapi.NewMessage(chatID, message)
msg.ParseMode = "Markdown"
sentMsg, err := bot.Send(msg)
if err != nil {
log.Printf("Failed to send message: %v", err)
return
}
lastMsgID = sentMsg.MessageID
}
func Run() {
log.Println("Sending initial price update...")
sendPriceUpdate()
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for {
select {
case <-ticker.C:
sendPriceUpdate()
}
}
}

193
service/guard.go Normal file
View File

@ -0,0 +1,193 @@
package service
import (
"fmt"
"log"
"os"
"strconv"
"sync"
"time"
"your-project-name/core"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
var (
botToken string
adminID int64
dbFile string
debugMode bool
)
func init() {
botToken = os.Getenv("BOT_TOKEN")
adminIDStr := os.Getenv("ADMIN_ID")
var err error
adminID, err = strconv.ParseInt(adminIDStr, 10, 64)
if err != nil {
log.Fatalf("Invalid ADMIN_ID: %v", err)
}
dbFile = "/app/data/q58.db" // 新的数据库文件路径
debugMode = os.Getenv("DEBUG_MODE") == "true"
}
type RateLimiter struct {
mu sync.Mutex
maxCalls int
period time.Duration
calls []time.Time
}
func NewRateLimiter(maxCalls int, period time.Duration) *RateLimiter {
return &RateLimiter{
maxCalls: maxCalls,
period: period,
calls: make([]time.Time, 0, maxCalls),
}
}
func (r *RateLimiter) Allow() bool {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
if len(r.calls) < r.maxCalls {
r.calls = append(r.calls, now)
return true
}
if now.Sub(r.calls[0]) >= r.period {
r.calls = append(r.calls[1:], now)
return true
}
return false
}
func deleteMessageAfterDelay(bot *tgbotapi.BotAPI, chatID int64, messageID int, delay time.Duration) {
time.Sleep(delay)
deleteMsg := tgbotapi.NewDeleteMessage(chatID, messageID)
_, err := bot.Request(deleteMsg)
if err != nil {
log.Printf("Failed to delete message: %v", err)
}
}
func processMessage(bot *tgbotapi.BotAPI, message *tgbotapi.Message, linkFilter *core.LinkFilter) {
if message.Chat.Type != "private" {
log.Printf("Processing message: %s", message.Text)
shouldFilter, newLinks := linkFilter.ShouldFilter(message.Text)
if shouldFilter {
log.Printf("Message should be filtered: %s", message.Text)
if message.From.ID != adminID {
deleteMsg := tgbotapi.NewDeleteMessage(message.Chat.ID, message.MessageID)
_, err := bot.Request(deleteMsg)
if err != nil {
log.Printf("Failed to delete message: %v", err)
}
notification := tgbotapi.NewMessage(message.Chat.ID, "已撤回该消息。注:一个链接不能发两次.")
sent, err := bot.Send(notification)
if err != nil {
log.Printf("Failed to send notification: %v", err)
} else {
go deleteMessageAfterDelay(bot, message.Chat.ID, sent.MessageID, 3*time.Minute)
}
}
return
}
if len(newLinks) > 0 {
log.Printf("New non-whitelisted links found: %v", newLinks)
}
}
}
func messageHandler(bot *tgbotapi.BotAPI, update tgbotapi.Update, linkFilter *core.LinkFilter, rateLimiter *RateLimiter) {
if update.Message == nil {
return
}
if update.Message.Chat.Type != "private" || update.Message.From.ID != adminID {
if rateLimiter.Allow() {
processMessage(bot, update.Message, linkFilter)
}
}
}
func commandHandler(bot *tgbotapi.BotAPI, update tgbotapi.Update, linkFilter *core.LinkFilter) {
if update.Message == nil || update.Message.Chat.Type != "private" || update.Message.From.ID != adminID {
return
}
linkFilter.LoadDataFromFile()
command := update.Message.Command()
args := update.Message.CommandArguments()
switch command {
case "add", "delete", "list", "deletecontaining":
linkFilter.HandleKeywordCommand(bot, update.Message, command, args)
case "addwhite", "delwhite", "listwhite":
linkFilter.HandleWhitelistCommand(bot, update.Message, command, args)
}
if command == "add" || command == "delete" || command == "deletecontaining" || command == "list" || command == "addwhite" || command == "delwhite" || command == "listwhite" {
linkFilter.LoadDataFromFile()
}
}
func StartBot() error {
bot, err := tgbotapi.NewBotAPI(botToken)
if err != nil {
return fmt.Errorf("failed to create bot: %w", err)
}
bot.Debug = debugMode
log.Printf("Authorized on account %s", bot.Self.UserName)
err = core.RegisterCommands(bot, adminID)
if err != nil {
return fmt.Errorf("error registering commands: %w", err)
}
linkFilter := core.NewLinkFilter(dbFile)
rateLimiter := NewRateLimiter(10, time.Second)
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
go messageHandler(bot, update, linkFilter, rateLimiter)
go commandHandler(bot, update, linkFilter)
}
return nil // 如果 bot 正常退出,返回 nil
}
func RunGuard() {
baseDelay := time.Second
maxDelay := 5 * time.Minute
delay := baseDelay
for {
err := StartBot()
if err != nil {
log.Printf("Bot encountered an error: %v", err)
log.Printf("Attempting to restart in %v...", delay)
time.Sleep(delay)
// 实现指数退避
delay *= 2
if delay > maxDelay {
delay = maxDelay
}
} else {
// 如果 bot 正常退出,重置延迟
delay = baseDelay
log.Println("Bot disconnected. Attempting to restart immediately...")
}
}
}