snapshot before merge
This commit is contained in:
1 parent
06a5f3da4d
commit
671871e0b2
145 files changed
+9786
-59
No files matched your search
@@ -0,0 +1,301 @@
|
||||
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, " · ")
|
||||
}
|
||||
Reference in new issue
Block a user