Files
blog/bot-go/internal/tgcore/tgcore.go
T
2026-06-01 17:57:22 +08:00

302 lines
6.2 KiB
Go

package tgcore
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
)
var API string
func Init(token string) {
API = "https://api.telegram.org/bot" + token
}
func TG(method string, body interface{}) (map[string]interface{}, error) {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, err
}
resp, err := http.Post(API+"/"+method, "application/json", bytes.NewReader(jsonBody))
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
return result, nil
}
func Allowed(chatID int64, allowedIDs []int64) bool {
if len(allowedIDs) == 0 {
return true
}
for _, id := range allowedIDs {
if id == chatID {
return true
}
}
return false
}
func SendMessage(chatID int64, text string, opts map[string]interface{}) error {
safeText := text
body := map[string]interface{}{
"chat_id": chatID,
"text": safeText,
"parse_mode": "HTML",
"disable_web_page_preview": true,
}
if opts != nil {
for k, v := range opts {
body[k] = v
}
}
_, err := TG("sendMessage", body)
if err != nil && (strings.Contains(err.Error(), "parse") || strings.Contains(err.Error(), "HTML")) {
delete(body, "parse_mode")
_, err = TG("sendMessage", body)
}
return err
}
// TgHandler is the callback handler interface
type TgHandler interface {
List(chatID int64, page string)
Links(chatID int64, page string)
Feeds(chatID int64, page string)
// Session callbacks
SessionCallback(chatID int64, action string)
MenuCallback(chatID int64, action string)
}
func HandleCallback(chatID int64, queryID string, data string, h TgHandler) {
if data == "" {
return
}
// Session callbacks
if strings.HasPrefix(data, "session_") {
action := strings.TrimPrefix(data, "session_")
if h != nil {
h.SessionCallback(chatID, action)
}
TG("answerCallbackQuery", map[string]interface{}{"callback_query_id": queryID})
return
}
// Menu callbacks
if strings.HasPrefix(data, "menu_") {
action := strings.TrimPrefix(data, "menu_")
if h != nil {
h.MenuCallback(chatID, action)
}
TG("answerCallbackQuery", map[string]interface{}{"callback_query_id": queryID})
return
}
// Pagination callbacks: list_N, links_N, feeds_N
parts := strings.SplitN(data, "_", 2)
if len(parts) == 2 {
cmd := parts[0]
page := parts[1]
if h != nil {
switch cmd {
case "list":
h.List(chatID, page)
case "links":
h.Links(chatID, page)
case "feeds":
h.Feeds(chatID, page)
}
}
}
TG("answerCallbackQuery", map[string]interface{}{"callback_query_id": queryID})
}
func GetInt64(m map[string]interface{}, key string) int64 {
if v, ok := m[key]; ok {
switch n := v.(type) {
case float64:
return int64(n)
case int64:
return n
}
}
return 0
}
func GetString(m map[string]interface{}, key string) string {
if v, ok := m[key]; ok {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// === Inlined from helpers ===
var lanIPCache struct {
ip string
ts time.Time
}
func GetLanIP(preferIface string) string {
if lanIPCache.ip != "" && time.Since(lanIPCache.ts) < 30*time.Second {
return lanIPCache.ip
}
ip := "0.0.0.0"
ifaces, err := net.Interfaces()
if err != nil {
lanIPCache.ip = ip
lanIPCache.ts = time.Now()
return ip
}
type candidate struct {
name string
address string
}
var candidates []candidate
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if !ok {
continue
}
ip4 := ipNet.IP.To4()
if ip4 == nil {
continue
}
if ip4.IsLoopback() {
continue
}
ipStr := ip4.String()
if strings.HasPrefix(ipStr, "169.254.") {
continue
}
candidates = append(candidates, candidate{name: iface.Name, address: ipStr})
}
}
prefLower := strings.ToLower(preferIface)
for _, c := range candidates {
if strings.Contains(strings.ToLower(c.name), prefLower) {
ip = c.address
lanIPCache.ip = ip
lanIPCache.ts = time.Now()
return ip
}
}
for _, c := range candidates {
n := strings.ToLower(c.name)
if !strings.Contains(n, "vmware") && !strings.Contains(n, "virtual") && !strings.Contains(n, "vethernet") {
ip = c.address
lanIPCache.ip = ip
lanIPCache.ts = time.Now()
return ip
}
}
if len(candidates) > 0 {
ip = candidates[0].address
}
lanIPCache.ip = ip
lanIPCache.ts = time.Now()
return ip
}
func CheckPort(port int) bool {
conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 1500*time.Millisecond)
if err != nil {
return false
}
conn.Close()
return true
}
func WaitForReady(port int, timeoutMs int) bool {
if timeoutMs <= 0 {
timeoutMs = 30000
}
deadline := time.Now().Add(time.Duration(timeoutMs) * time.Millisecond)
for time.Now().Before(deadline) {
if CheckPort(port) {
return true
}
time.Sleep(1500 * time.Millisecond)
}
return false
}
func MakeSlug(title string) string {
dateStr := time.Now().Format("2006-01-02")
slugPart := title
// Replace non-word, non-CJK chars with -
slugPart = strings.Map(func(r rune) rune {
if (r >= 0x4e00 && r <= 0x9fff) || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' {
return r
}
return '-'
}, slugPart)
slugPart = strings.Trim(slugPart, "-")
slugPart = strings.ToLower(slugPart)
if slugPart == "" {
slugPart = "post"
}
return dateStr + "-" + slugPart
}
func SessionSummary(title string, categories, tags []string, content string) string {
lines := 0
chars := len([]rune(content))
for _, ch := range content {
if ch == '\n' {
lines++
}
}
if content != "" {
lines++
}
displayTitle := title
if displayTitle == "" {
displayTitle = "未命名"
}
parts := []string{
fmt.Sprintf("📝 <b>%s</b>", displayTitle),
}
if len(categories) > 0 {
parts = append(parts, "📂 "+strings.Join(categories, "、"))
}
if len(tags) > 0 {
parts = append(parts, "🏷 "+strings.Join(tags, "、"))
}
if chars > 0 {
parts = append(parts, fmt.Sprintf("✍ %d 行 %d 字", lines, chars))
}
return strings.Join(parts, " · ")
}