mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 11:37:06 +08:00
222 lines
5.7 KiB
Go
222 lines
5.7 KiB
Go
package messaging
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"mime"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Wxw-Gu/WechatExplorer/services/wechat-connector/ilink"
|
|
)
|
|
|
|
// reMarkdownImage matches markdown image syntax: 
|
|
var reMarkdownImage = regexp.MustCompile(`!\[[^\]]*\]\(([^)]+)\)`)
|
|
|
|
// ExtractImageURLs extracts image URLs from markdown text.
|
|
func ExtractImageURLs(text string) []string {
|
|
matches := reMarkdownImage.FindAllStringSubmatch(text, -1)
|
|
var urls []string
|
|
for _, m := range matches {
|
|
url := strings.TrimSpace(m[1])
|
|
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
|
|
urls = append(urls, url)
|
|
}
|
|
}
|
|
return urls
|
|
}
|
|
|
|
// SendMediaFromURL sends a local file or downloads from a URL and sends it as a media message.
|
|
func SendMediaFromURL(ctx context.Context, client *ilink.Client, toUserID, mediaURL, contextToken string) error {
|
|
// Check if it's a local file
|
|
if _, err := os.Stat(mediaURL); err == nil {
|
|
return SendMediaFromPath(ctx, client, toUserID, mediaURL, contextToken)
|
|
}
|
|
// Must be a valid HTTP URL to download
|
|
if !strings.HasPrefix(mediaURL, "http://") && !strings.HasPrefix(mediaURL, "https://") {
|
|
return fmt.Errorf("unsupported media path (not a local file and not an HTTP URL): %s", mediaURL)
|
|
}
|
|
data, contentType, err := downloadFile(ctx, mediaURL)
|
|
if err != nil {
|
|
return fmt.Errorf("download %s: %w", mediaURL, err)
|
|
}
|
|
|
|
return sendMediaData(ctx, client, toUserID, filenameFromURL(mediaURL), mediaURL, data, contentType, contextToken)
|
|
}
|
|
|
|
// SendMediaFromPath reads a local file and sends it as a media message.
|
|
func SendMediaFromPath(ctx context.Context, client *ilink.Client, toUserID, path, contextToken string) error {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("read %s: %w", path, err)
|
|
}
|
|
|
|
return sendMediaData(ctx, client, toUserID, filepath.Base(path), path, data, inferContentType(path), contextToken)
|
|
}
|
|
|
|
func sendMediaData(ctx context.Context, client *ilink.Client, toUserID, fileName, source string, data []byte, contentType, contextToken string) error {
|
|
if fileName == "" {
|
|
fileName = "file"
|
|
}
|
|
|
|
cdnMediaType, itemType := classifyMedia(contentType, source)
|
|
|
|
log.Printf("[media] uploading %s (%s, %d bytes) for %s", source, contentType, len(data), toUserID)
|
|
|
|
uploaded, err := UploadFileToCDN(ctx, client, data, toUserID, cdnMediaType)
|
|
if err != nil {
|
|
return fmt.Errorf("upload to CDN: %w", err)
|
|
}
|
|
|
|
media := &ilink.MediaInfo{
|
|
EncryptQueryParam: uploaded.DownloadParam,
|
|
AESKey: AESKeyToBase64(uploaded.AESKeyHex),
|
|
EncryptType: 1,
|
|
}
|
|
|
|
var item ilink.MessageItem
|
|
switch itemType {
|
|
case ilink.ItemTypeImage:
|
|
item = ilink.MessageItem{
|
|
Type: ilink.ItemTypeImage,
|
|
ImageItem: &ilink.ImageItem{
|
|
Media: media,
|
|
MidSize: uploaded.CipherSize,
|
|
},
|
|
}
|
|
case ilink.ItemTypeVideo:
|
|
item = ilink.MessageItem{
|
|
Type: ilink.ItemTypeVideo,
|
|
VideoItem: &ilink.VideoItem{
|
|
Media: media,
|
|
VideoSize: uploaded.CipherSize,
|
|
},
|
|
}
|
|
default:
|
|
item = ilink.MessageItem{
|
|
Type: ilink.ItemTypeFile,
|
|
FileItem: &ilink.FileItem{
|
|
Media: media,
|
|
FileName: fileName,
|
|
Len: fmt.Sprintf("%d", uploaded.FileSize),
|
|
},
|
|
}
|
|
}
|
|
|
|
req := &ilink.SendMessageRequest{
|
|
Msg: ilink.SendMsg{
|
|
FromUserID: client.BotID(),
|
|
ToUserID: toUserID,
|
|
ClientID: NewClientID(),
|
|
MessageType: ilink.MessageTypeBot,
|
|
MessageState: ilink.MessageStateFinish,
|
|
ItemList: []ilink.MessageItem{item},
|
|
ContextToken: contextToken,
|
|
},
|
|
BaseInfo: ilink.BaseInfo{},
|
|
}
|
|
|
|
resp, err := client.SendMessage(ctx, req)
|
|
if err != nil {
|
|
return fmt.Errorf("send media message: %w", err)
|
|
}
|
|
if resp.Ret != 0 {
|
|
return fmt.Errorf("send media failed: ret=%d errmsg=%s", resp.Ret, resp.ErrMsg)
|
|
}
|
|
|
|
log.Printf("[media] sent %s to %s from %s", contentType, toUserID, source)
|
|
return nil
|
|
}
|
|
|
|
func downloadFile(ctx context.Context, url string) ([]byte, string, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, "", fmt.Errorf("HTTP %d", resp.StatusCode)
|
|
}
|
|
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
contentType := resp.Header.Get("Content-Type")
|
|
if contentType == "" {
|
|
contentType = inferContentType(url)
|
|
}
|
|
|
|
return data, contentType, nil
|
|
}
|
|
|
|
func classifyMedia(contentType, url string) (cdnMediaType int, itemType int) {
|
|
ct := strings.ToLower(contentType)
|
|
|
|
if strings.HasPrefix(ct, "image/") || isImageExt(url) {
|
|
return ilink.CDNMediaTypeImage, ilink.ItemTypeImage
|
|
}
|
|
if strings.HasPrefix(ct, "video/") || isVideoExt(url) {
|
|
return ilink.CDNMediaTypeVideo, ilink.ItemTypeVideo
|
|
}
|
|
return ilink.CDNMediaTypeFile, ilink.ItemTypeFile
|
|
}
|
|
|
|
func isImageExt(url string) bool {
|
|
ext := strings.ToLower(filepath.Ext(stripQuery(url)))
|
|
switch ext {
|
|
case ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isVideoExt(url string) bool {
|
|
ext := strings.ToLower(filepath.Ext(stripQuery(url)))
|
|
switch ext {
|
|
case ".mp4", ".mov", ".webm", ".mkv", ".avi":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func inferContentType(url string) string {
|
|
ext := filepath.Ext(stripQuery(url))
|
|
if ct := mime.TypeByExtension(ext); ct != "" {
|
|
return ct
|
|
}
|
|
return "application/octet-stream"
|
|
}
|
|
|
|
func filenameFromURL(rawURL string) string {
|
|
u := stripQuery(rawURL)
|
|
name := filepath.Base(u)
|
|
if name == "" || name == "." || name == "/" {
|
|
return "file"
|
|
}
|
|
return name
|
|
}
|
|
|
|
func stripQuery(rawURL string) string {
|
|
if i := strings.IndexByte(rawURL, '?'); i >= 0 {
|
|
return rawURL[:i]
|
|
}
|
|
return rawURL
|
|
}
|