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("📝 %s", 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, " · ") }