Files
wechat_chatter/onebot/http.go
T
2026-07-09 09:42:32 +08:00

261 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"runtime/debug"
"strings"
"time"
)
func sendHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "仅支持 POST", http.StatusMethodNotAllowed)
Error("仅支持 POST")
return
}
req := new(SendRequest)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
http.Error(w, "无效的 JSON", http.StatusBadRequest)
Error("无效的 JSON")
return
}
// 参数校验
if len(req.Message) == 0 || (req.UserID == "" && req.GroupID == "") {
http.Error(w, "参数缺失", http.StatusBadRequest)
Error("参数缺失")
return
}
sendContent := ""
atUserID := ""
var resultChans []chan error
for _, v := range req.Message {
if v.Type == "text" {
sendContent += v.Data.Text
} else if v.Type == "at" {
if req.GroupID != "" {
if nicknameInter, ok := userID2NicknameMap.Load(req.GroupID + "_" + v.Data.QQ); ok {
sendContent += fmt.Sprintf("@%s\u2005", nicknameInter.(string))
atUserID += v.Data.QQ + ","
}
}
} else if v.Type == "image" || v.Type == "video" || v.Type == "record" || v.Type == "voice" || v.Type == "file" {
msgType := v.Type
if msgType == "record" || msgType == "voice" {
msgType = "voice"
}
// file: 走 iPad860 风格 uploadappattach 直传(不走 CDN)
if msgType == "file" {
msgType = "send_file_simple"
}
ch := make(chan error, 1)
msg := &SendMsg{
UserId: req.UserID,
GroupID: req.GroupID,
Content: v.Data.File,
Type: msgType,
ResultChan: ch,
}
msgChan <- msg
resultChans = append(resultChans, ch)
} else if v.Type == "reply" {
if v.Data.ReplyMessage == nil {
Error("reply_message为空")
continue
}
rm := v.Data.ReplyMessage
// 顶层没有group_id时,从reply_message中取
if req.GroupID == "" && rm.GroupId != "" {
req.GroupID = rm.GroupId
}
// 提取被回复消息的内容
referContent := ""
referMsgType := 1 // 默认text
if len(rm.Message) > 0 {
switch rm.Message[0].Type {
case "text":
referContent = rm.Message[0].Data.Text
referMsgType = 1
case "image":
referMsgType = 3
case "video":
referMsgType = 43
case "file":
referMsgType = 49
}
}
// 提取发送者昵称
displayName := ""
if rm.Sender != nil {
displayName = rm.Sender.Nickname
}
// msgsource需要JSON unescape(双重编码: \\u003c → \u003c → <
msgsource := jsonUnescapeString(rm.MsgResource)
ch := make(chan error, 1)
msgChan <- &SendMsg{
UserId: req.UserID,
GroupID: req.GroupID,
Content: v.Data.Text,
Type: "reply",
ReferMsgId: rm.MessageId,
ReferMsgSender: rm.UserID,
ReferMsgType: referMsgType,
ReferCreateTime: rm.Time,
ReferMsgsource: msgsource,
ReferDisplayName: displayName,
ReferContent: referContent,
ResultChan: ch,
}
resultChans = append(resultChans, ch)
}
}
if sendContent != "" {
ch := make(chan error, 1)
msgChan <- &SendMsg{
UserId: req.UserID,
GroupID: req.GroupID,
Content: sendContent,
Type: "text",
AtUser: strings.TrimRight(atUserID, ","),
ResultChan: ch,
}
resultChans = append(resultChans, ch)
}
// 等待所有消息发送完成
for _, ch := range resultChans {
if err := <-ch; err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]any{
"status": "failed",
"error": err.Error(),
})
return
}
}
json.NewEncoder(w).Encode(map[string]any{
"status": "ok",
})
}
func SendHttpReq(jsonData []byte) {
defer func() {
if r := recover(); r != nil {
Error("http panic", "err", r, "stack", string(debug.Stack()))
}
}()
time.Sleep(time.Duration(config.SendInterval) * time.Millisecond)
jsonReq, err := HandleMsg(jsonData)
if err != nil {
Error("JSON 序列化失败", "err", err)
return
}
if jsonReq == nil {
return
}
Info("发送数据", "msg", string(jsonReq))
req, err := http.NewRequest("POST", config.SendURL, bytes.NewBuffer(jsonReq))
if err != nil {
Error("创建请求失败", "err", err)
return
}
// 5. 设置 Header (OneBot 接口通常要求 application/json)
h := hmac.New(sha1.New, []byte(config.OnebotToken))
h.Write(jsonReq)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Signature", "sha1="+hex.EncodeToString(h.Sum(nil)))
client := &http.Client{
Timeout: 10 * time.Second,
}
// 6. 执行请求
resp, err := client.Do(req)
if err != nil {
Error("请求执行失败", "err", err)
return
}
defer resp.Body.Close()
// 7. 读取返回结果
body, err := io.ReadAll(resp.Body)
if err != nil {
Error("读取响应失败", "err", err)
return
}
Info("返回内容", "status", resp.StatusCode, "body", string(body))
}
// jsonUnescapeString 对双重JSON编码的字符串做unescape
// 例如: \\u003c → \u003c (第一次json.Unmarshal) → < (本函数)
func jsonUnescapeString(s string) string {
if s == "" {
return s
}
var result string
if err := json.Unmarshal([]byte(`"`+s+`"`), &result); err != nil {
return s
}
return result
}
// getFileExt 从文件名中提取扩展名(不含.)
func getFileExt(fileName string) string {
idx := strings.LastIndex(fileName, ".")
if idx == -1 || idx == len(fileName)-1 {
return ""
}
return fileName[idx+1:]
}
// extractFileName 从路径或URL中提取文件名
func extractFileName(s string) string {
if s == "" {
return ""
}
// 跳过 base64 数据
if strings.HasPrefix(s, "base64://") || strings.Contains(s, ";base64,") {
return ""
}
// URL 路径
if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") || strings.HasPrefix(s, "file://") {
if u, err := url.Parse(s); err == nil {
p := u.Path
if idx := strings.LastIndex(p, "/"); idx != -1 {
return p[idx+1:]
}
return p
}
}
// 本地文件路径
if strings.Contains(s, "/") || strings.Contains(s, "\\") {
if idx := strings.LastIndexAny(s, "/\\"); idx != -1 {
return s[idx+1:]
}
}
return s
}