mirror of
https://github.com/yincongcyincong/wechat_chatter.git
synced 2026-07-15 10:26:52 +08:00
261 lines
6.2 KiB
Go
261 lines
6.2 KiB
Go
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
|
||
}
|
||
|