302 lines
6.2 KiB
Go
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, " · ")
|
|
}
|