feat: upload local QQ attachments before sending
This commit is contained in:
parent
e41423483e
commit
a752b00247
3 changed files with 377 additions and 62 deletions
|
|
@ -199,20 +199,7 @@ func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
||||||
msgToCreate.Content = ""
|
msgToCreate.Content = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach passive reply msg_id and msg_seq if available.
|
c.applyReplyContext(msg.ChatID, msgToCreate)
|
||||||
if v, ok := c.lastMsgID.Load(msg.ChatID); ok {
|
|
||||||
if msgID, ok := v.(string); ok && msgID != "" {
|
|
||||||
msgToCreate.MsgID = msgID
|
|
||||||
|
|
||||||
// Increment msg_seq atomically for multi-part replies.
|
|
||||||
if counterVal, ok := c.msgSeqCounters.Load(msg.ChatID); ok {
|
|
||||||
if counter, ok := counterVal.(*atomic.Uint64); ok {
|
|
||||||
seq := counter.Add(1)
|
|
||||||
msgToCreate.MsgSeq = uint32(seq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize URLs in group messages to avoid QQ's URL blacklist rejection.
|
// Sanitize URLs in group messages to avoid QQ's URL blacklist rejection.
|
||||||
if chatKind == "group" {
|
if chatKind == "group" {
|
||||||
|
|
@ -244,6 +231,22 @@ func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *QQChannel) applyReplyContext(chatID string, msgToCreate *dto.MessageToCreate) {
|
||||||
|
if v, ok := c.lastMsgID.Load(chatID); ok {
|
||||||
|
if msgID, ok := v.(string); ok && msgID != "" {
|
||||||
|
msgToCreate.MsgID = msgID
|
||||||
|
|
||||||
|
// Increment msg_seq atomically for multi-part replies.
|
||||||
|
if counterVal, ok := c.msgSeqCounters.Load(chatID); ok {
|
||||||
|
if counter, ok := counterVal.(*atomic.Uint64); ok {
|
||||||
|
seq := counter.Add(1)
|
||||||
|
msgToCreate.MsgSeq = uint32(seq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// StartTyping implements channels.TypingCapable.
|
// StartTyping implements channels.TypingCapable.
|
||||||
// It sends an InputNotify (msg_type=6) immediately and re-sends every 8 seconds.
|
// It sends an InputNotify (msg_type=6) immediately and re-sends every 8 seconds.
|
||||||
// The returned stop function is idempotent and cancels the goroutine.
|
// The returned stop function is idempotent and cancels the goroutine.
|
||||||
|
|
@ -316,71 +319,60 @@ func (c *QQChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessage)
|
||||||
chatKind := c.getChatKind(msg.ChatID)
|
chatKind := c.getChatKind(msg.ChatID)
|
||||||
|
|
||||||
for _, part := range msg.Parts {
|
for _, part := range msg.Parts {
|
||||||
// If the ref is already an HTTP(S) URL, use it directly.
|
if isHTTPURL(part.Ref) {
|
||||||
mediaURL := part.Ref
|
richMedia := &dto.RichMediaMessage{
|
||||||
if !isHTTPURL(mediaURL) {
|
FileType: qqFileType(part.Type),
|
||||||
// Try resolving through media store.
|
URL: part.Ref,
|
||||||
store := c.GetMediaStore()
|
SrvSendMsg: true,
|
||||||
if store == nil {
|
|
||||||
logger.WarnCF("qq", "QQ media requires HTTP/HTTPS URL, no media store available", map[string]any{
|
|
||||||
"ref": part.Ref,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resolved, err := store.Resolve(part.Ref)
|
var sendErr error
|
||||||
if err != nil {
|
if chatKind == "group" {
|
||||||
logger.ErrorCF("qq", "Failed to resolve media ref", map[string]any{
|
_, sendErr = c.api.PostGroupMessage(ctx, msg.ChatID, richMedia)
|
||||||
"ref": part.Ref,
|
} else {
|
||||||
"error": err.Error(),
|
_, sendErr = c.api.PostC2CMessage(ctx, msg.ChatID, richMedia)
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isHTTPURL(resolved) {
|
if sendErr != nil {
|
||||||
logger.WarnCF("qq", "QQ media requires HTTP/HTTPS URL, local files not supported", map[string]any{
|
logger.ErrorCF("qq", "Failed to send remote media", map[string]any{
|
||||||
"ref": part.Ref,
|
"type": part.Type,
|
||||||
"resolved": resolved,
|
"chat_id": msg.ChatID,
|
||||||
|
"error": sendErr.Error(),
|
||||||
})
|
})
|
||||||
continue
|
return fmt.Errorf("qq send media: %w", channels.ErrTemporary)
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
mediaURL = resolved
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map part type to QQ file type: 1=image, 2=video, 3=audio, 4=file.
|
store := c.GetMediaStore()
|
||||||
var fileType uint64
|
if store == nil {
|
||||||
switch part.Type {
|
return fmt.Errorf("qq send media: media store not configured for local media ref %q", part.Ref)
|
||||||
case "image":
|
|
||||||
fileType = 1
|
|
||||||
case "video":
|
|
||||||
fileType = 2
|
|
||||||
case "audio":
|
|
||||||
fileType = 3
|
|
||||||
default:
|
|
||||||
fileType = 4 // file
|
|
||||||
}
|
}
|
||||||
|
|
||||||
richMedia := &dto.RichMediaMessage{
|
resolved, err := store.Resolve(part.Ref)
|
||||||
FileType: fileType,
|
if err != nil {
|
||||||
URL: mediaURL,
|
return fmt.Errorf("qq send media: resolve local media ref %q: %w", part.Ref, err)
|
||||||
SrvSendMsg: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var sendErr error
|
fileInfo, err := c.uploadLocalMedia(ctx, chatKind, msg.ChatID, part.Type, resolved)
|
||||||
if chatKind == "group" {
|
if err != nil {
|
||||||
_, sendErr = c.api.PostGroupMessage(ctx, msg.ChatID, richMedia)
|
logger.ErrorCF("qq", "Failed to upload local media", map[string]any{
|
||||||
} else {
|
"type": part.Type,
|
||||||
_, sendErr = c.api.PostC2CMessage(ctx, msg.ChatID, richMedia)
|
"chat_id": msg.ChatID,
|
||||||
|
"ref": part.Ref,
|
||||||
|
"resolved": resolved,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("qq send media: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if sendErr != nil {
|
if err := c.sendUploadedMedia(ctx, chatKind, msg.ChatID, part, fileInfo); err != nil {
|
||||||
logger.ErrorCF("qq", "Failed to send media", map[string]any{
|
logger.ErrorCF("qq", "Failed to send uploaded media", map[string]any{
|
||||||
"type": part.Type,
|
"type": part.Type,
|
||||||
"chat_id": msg.ChatID,
|
"chat_id": msg.ChatID,
|
||||||
"error": sendErr.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return fmt.Errorf("qq send media: %w", channels.ErrTemporary)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,20 @@ package qq
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tencent-connect/botgo/dto"
|
"github.com/tencent-connect/botgo/dto"
|
||||||
|
"github.com/tencent-connect/botgo/openapi"
|
||||||
|
"github.com/tencent-connect/botgo/openapi/options"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
"github.com/sipeed/picoclaw/pkg/channels"
|
"github.com/sipeed/picoclaw/pkg/channels"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/media"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHandleC2CMessage_IncludesAccountIDMetadata(t *testing.T) {
|
func TestHandleC2CMessage_IncludesAccountIDMetadata(t *testing.T) {
|
||||||
|
|
@ -42,3 +49,219 @@ func TestHandleC2CMessage_IncludesAccountIDMetadata(t *testing.T) {
|
||||||
t.Fatalf("account_id metadata = %q, want %q", inbound.Metadata["account_id"], "7750283E123456")
|
t.Fatalf("account_id metadata = %q, want %q", inbound.Metadata["account_id"], "7750283E123456")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fakeQQAPI struct {
|
||||||
|
openapi.OpenAPI
|
||||||
|
|
||||||
|
transportCalls []transportCall
|
||||||
|
transportResp []byte
|
||||||
|
transportErr error
|
||||||
|
|
||||||
|
groupMessages []dto.APIMessage
|
||||||
|
groupErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
type transportCall struct {
|
||||||
|
method string
|
||||||
|
url string
|
||||||
|
body any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeQQAPI) Transport(ctx context.Context, method, url string, body interface{}) ([]byte, error) {
|
||||||
|
f.transportCalls = append(f.transportCalls, transportCall{
|
||||||
|
method: method,
|
||||||
|
url: url,
|
||||||
|
body: body,
|
||||||
|
})
|
||||||
|
return f.transportResp, f.transportErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeQQAPI) PostGroupMessage(
|
||||||
|
ctx context.Context,
|
||||||
|
groupID string,
|
||||||
|
msg dto.APIMessage,
|
||||||
|
opt ...options.Option,
|
||||||
|
) (*dto.Message, error) {
|
||||||
|
f.groupMessages = append(f.groupMessages, msg)
|
||||||
|
if f.groupErr != nil {
|
||||||
|
return nil, f.groupErr
|
||||||
|
}
|
||||||
|
return &dto.Message{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendMedia_LocalFileUploadsThenSendsRichMediaMessage(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
pdfPath := filepath.Join(tmpDir, "report.pdf")
|
||||||
|
if err := os.WriteFile(pdfPath, []byte("%PDF-1.4 test"), 0o644); err != nil {
|
||||||
|
t.Fatalf("write file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
store := media.NewFileMediaStore()
|
||||||
|
ref, err := store.Store(pdfPath, media.MediaMeta{
|
||||||
|
Filename: "report.pdf",
|
||||||
|
ContentType: "application/pdf",
|
||||||
|
Source: "test",
|
||||||
|
}, "scope")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("store media: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadedFileInfo := []byte("uploaded-file-info")
|
||||||
|
respBody, err := json.Marshal(struct {
|
||||||
|
FileInfo []byte `json:"file_info"`
|
||||||
|
}{
|
||||||
|
FileInfo: uploadedFileInfo,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
api := &fakeQQAPI{transportResp: respBody}
|
||||||
|
ch := &QQChannel{
|
||||||
|
BaseChannel: channels.NewBaseChannel("qq", nil, bus.NewMessageBus(), nil),
|
||||||
|
api: api,
|
||||||
|
}
|
||||||
|
ch.SetRunning(true)
|
||||||
|
ch.SetMediaStore(store)
|
||||||
|
ch.chatType.Store("group-1", "group")
|
||||||
|
|
||||||
|
err = ch.SendMedia(context.Background(), bus.OutboundMediaMessage{
|
||||||
|
ChatID: "group-1",
|
||||||
|
Parts: []bus.MediaPart{
|
||||||
|
{Ref: ref, Type: "file"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SendMedia() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(api.transportCalls) != 1 {
|
||||||
|
t.Fatalf("transport call count = %d, want 1", len(api.transportCalls))
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, ok := api.transportCalls[0].body.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("transport body type = %T, want map[string]any", api.transportCalls[0].body)
|
||||||
|
}
|
||||||
|
switch v := payload["file_type"].(type) {
|
||||||
|
case int:
|
||||||
|
if v != 4 {
|
||||||
|
t.Fatalf("file_type = %v, want 4", payload["file_type"])
|
||||||
|
}
|
||||||
|
case float64:
|
||||||
|
if v != 4 {
|
||||||
|
t.Fatalf("file_type = %v, want 4", payload["file_type"])
|
||||||
|
}
|
||||||
|
case uint64:
|
||||||
|
if v != 4 {
|
||||||
|
t.Fatalf("file_type = %v, want 4", payload["file_type"])
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("file_type type = %T, want numeric 4", payload["file_type"])
|
||||||
|
}
|
||||||
|
if payload["srv_send_msg"] != false {
|
||||||
|
t.Fatalf("srv_send_msg = %v, want false", payload["srv_send_msg"])
|
||||||
|
}
|
||||||
|
fileData, _ := payload["file_data"].(string)
|
||||||
|
if fileData == "" {
|
||||||
|
t.Fatal("file_data is empty, want base64-encoded local file contents")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(api.groupMessages) != 1 {
|
||||||
|
t.Fatalf("group message count = %d, want 1", len(api.groupMessages))
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, ok := api.groupMessages[0].(*dto.MessageToCreate)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("group message type = %T, want *dto.MessageToCreate", api.groupMessages[0])
|
||||||
|
}
|
||||||
|
if msg.MsgType != dto.RichMediaMsg {
|
||||||
|
t.Fatalf("msg_type = %v, want %v", msg.MsgType, dto.RichMediaMsg)
|
||||||
|
}
|
||||||
|
if msg.Media == nil {
|
||||||
|
t.Fatal("msg.Media is nil, want uploaded file_info")
|
||||||
|
}
|
||||||
|
if string(msg.Media.FileInfo) != string(uploadedFileInfo) {
|
||||||
|
t.Fatalf("file_info = %q, want %q", string(msg.Media.FileInfo), string(uploadedFileInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendMedia_LocalFileUploadFailureReturnsError(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
filePath := filepath.Join(tmpDir, "notes.txt")
|
||||||
|
if err := os.WriteFile(filePath, []byte("hello"), 0o644); err != nil {
|
||||||
|
t.Fatalf("write file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
store := media.NewFileMediaStore()
|
||||||
|
ref, err := store.Store(filePath, media.MediaMeta{
|
||||||
|
Filename: "notes.txt",
|
||||||
|
ContentType: "text/plain",
|
||||||
|
Source: "test",
|
||||||
|
}, "scope")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("store media: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
api := &fakeQQAPI{transportErr: errors.New("upload failed")}
|
||||||
|
ch := &QQChannel{
|
||||||
|
BaseChannel: channels.NewBaseChannel("qq", nil, bus.NewMessageBus(), nil),
|
||||||
|
api: api,
|
||||||
|
}
|
||||||
|
ch.SetRunning(true)
|
||||||
|
ch.SetMediaStore(store)
|
||||||
|
ch.chatType.Store("group-1", "group")
|
||||||
|
|
||||||
|
err = ch.SendMedia(context.Background(), bus.OutboundMediaMessage{
|
||||||
|
ChatID: "group-1",
|
||||||
|
Parts: []bus.MediaPart{
|
||||||
|
{Ref: ref, Type: "file"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("SendMedia() error = nil, want upload failure")
|
||||||
|
}
|
||||||
|
if len(api.groupMessages) != 0 {
|
||||||
|
t.Fatalf("group message count = %d, want 0 after upload failure", len(api.groupMessages))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendMedia_RemoteURLStillUsesRichMediaDirectSend(t *testing.T) {
|
||||||
|
api := &fakeQQAPI{}
|
||||||
|
ch := &QQChannel{
|
||||||
|
BaseChannel: channels.NewBaseChannel("qq", nil, bus.NewMessageBus(), nil),
|
||||||
|
api: api,
|
||||||
|
}
|
||||||
|
ch.SetRunning(true)
|
||||||
|
ch.chatType.Store("group-1", "group")
|
||||||
|
|
||||||
|
err := ch.SendMedia(context.Background(), bus.OutboundMediaMessage{
|
||||||
|
ChatID: "group-1",
|
||||||
|
Parts: []bus.MediaPart{
|
||||||
|
{
|
||||||
|
Ref: "https://example.com/report.pdf",
|
||||||
|
Type: "file",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SendMedia() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(api.transportCalls) != 0 {
|
||||||
|
t.Fatalf("transport call count = %d, want 0 for remote URL", len(api.transportCalls))
|
||||||
|
}
|
||||||
|
if len(api.groupMessages) != 1 {
|
||||||
|
t.Fatalf("group message count = %d, want 1", len(api.groupMessages))
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, ok := api.groupMessages[0].(*dto.RichMediaMessage)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("group message type = %T, want *dto.RichMediaMessage", api.groupMessages[0])
|
||||||
|
}
|
||||||
|
if msg.URL != "https://example.com/report.pdf" {
|
||||||
|
t.Fatalf("URL = %q, want %q", msg.URL, "https://example.com/report.pdf")
|
||||||
|
}
|
||||||
|
if !msg.SrvSendMsg {
|
||||||
|
t.Fatal("SrvSendMsg = false, want true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
100
pkg/channels/qq/upload.go
Normal file
100
pkg/channels/qq/upload.go
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
package qq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/tencent-connect/botgo/constant"
|
||||||
|
"github.com/tencent-connect/botgo/dto"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/channels"
|
||||||
|
)
|
||||||
|
|
||||||
|
func qqFileType(mediaType string) uint64 {
|
||||||
|
switch mediaType {
|
||||||
|
case "image":
|
||||||
|
return 1
|
||||||
|
case "video":
|
||||||
|
return 2
|
||||||
|
case "audio":
|
||||||
|
return 3
|
||||||
|
default:
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *QQChannel) uploadLocalMedia(
|
||||||
|
ctx context.Context,
|
||||||
|
chatKind string,
|
||||||
|
chatID string,
|
||||||
|
partType string,
|
||||||
|
localPath string,
|
||||||
|
) ([]byte, error) {
|
||||||
|
content, err := os.ReadFile(localPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read local media: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := map[string]any{
|
||||||
|
"file_type": qqFileType(partType),
|
||||||
|
"srv_send_msg": false,
|
||||||
|
"file_data": base64.StdEncoding.EncodeToString(content),
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := c.api.Transport(ctx, http.MethodPost, c.uploadURL(chatKind, chatID), payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("upload local media: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploaded dto.Message
|
||||||
|
if err := json.Unmarshal(respBody, &uploaded); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode upload response: %w", err)
|
||||||
|
}
|
||||||
|
if len(uploaded.FileInfo) == 0 {
|
||||||
|
return nil, fmt.Errorf("upload local media: empty file_info")
|
||||||
|
}
|
||||||
|
|
||||||
|
return uploaded.FileInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *QQChannel) uploadURL(chatKind, chatID string) string {
|
||||||
|
if chatKind == "group" {
|
||||||
|
return fmt.Sprintf("%s/v2/groups/%s/files", constant.APIDomain, chatID)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/v2/users/%s/files", constant.APIDomain, chatID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *QQChannel) sendUploadedMedia(
|
||||||
|
ctx context.Context,
|
||||||
|
chatKind string,
|
||||||
|
chatID string,
|
||||||
|
part bus.MediaPart,
|
||||||
|
fileInfo []byte,
|
||||||
|
) error {
|
||||||
|
msgToCreate := &dto.MessageToCreate{
|
||||||
|
Content: part.Caption,
|
||||||
|
MsgType: dto.RichMediaMsg,
|
||||||
|
Media: &dto.MediaInfo{
|
||||||
|
FileInfo: fileInfo,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c.applyReplyContext(chatID, msgToCreate)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if chatKind == "group" {
|
||||||
|
_, err = c.api.PostGroupMessage(ctx, chatID, msgToCreate)
|
||||||
|
} else {
|
||||||
|
_, err = c.api.PostC2CMessage(ctx, chatID, msgToCreate)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("qq send uploaded media: %w", channels.ErrTemporary)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue